学海苦舟-Java 核心技术 -《写给大忙人看的Java核心技术》+ 《Java 核心》 综合总结

目标:与 《Java 核心技术》相辅相成, 多方面精炼 Java 核心

2021-02-16


并发编程

p326

1.与使用锁机制编程相比,并行算法和线程安全的数据结构更好。

2.并行Stream 和 数组操作可以自动并且安全地将计算并行化运行.

3.长期运行的任务不应阻塞程序的UI, 但是进度和最终的更新需要在UI 线程中出现。


线程是执行一系列指令的机制,通常由操作系统提供,通过使用单独的处理器或者同一个处理器的不同时间片,多个线程并发运行。

在实际中,让任务和线程之间维持一对一的关系通常是没有意义的。当任务执行只需要很短的时间时。你想在同一个线程上执行这些任务,不像浪费启动线程的时间。

当任务时计算密集型时,想让每个处理器只有一个线程,而不是每个任务一个线程,以避免线程之间切换的开销。


 Executors.newCachedThreadPool() : 一个有很多短暂的任务或者任务会消耗很多时间等待的程序优化过的 executor. 如果可能的化,每个任务在空闲的线程上执行,

如果所有的线程都在繁忙工作, 就会分配一个新线程,超过一定空闲时间的线程会被终止.

Executors.newFixedThreadPool(nthreads) :  数目固定的线程池。 提交任务,进行排队直到有可用的线程。对计算密集型任务适用。

int processors = Runtime.getRuntime().availableProcessors() . 可用的处理器数目,推断出线程数。

N(threads) =  N(cpu) *  U( % util radio) * ( 1 + W wait / C comput )  

计算密集型 N(threads ) + 1 ;

IO 密集型 N (threads )



 

主调任务会被阻塞,直到所有子任务都完成。

如果不满足需求,使用 ExectorCompletionSERVICE,会以完成顺序返回future.

 搜索到一个满足要求的情景:

 


 线程安全性:

1. 不可见

2. 状态的更改

对变量的更新可见

1. final 变量的值在初始化后是可见的。

2. static 变量的初始值在静态初始化之后是可见的。

3. volatile 变量得改变时可见得。

4.发生在锁被释放之前得改变对任何试图获取同一个锁得任何人时可见得。


 

使用锁使得操作得灵界序列称为不可分得原子操作。

不是解决并发问题得通用方案,很难恰当使用它们,容易错误使用它们而导致性能严重下降或甚至导致“死锁”。


并发编程安全策略

1 。卓有成效得策略是限制。要在任务中间共享数据时,no, 局部私有,然后合并结果

2. 不变性,共享不可变对象是安全得。

3.锁。 授权一次只有一个任务访问数据结构,避免数据结构被损坏。

锁代价很高,减少了并行执行得机会。

将数据分区,并发访问不同得部分,

JUC 一些数据结构就用到了分区,Stream 类库中得并发算法也是这样做的.


不可变类

不可变集合总是创建一个新的集合,这样更新结果。

results = results.union(newResults);

实现:

1. 确保将实例变量声明为final。虚拟机确保final实例变量在构造之后是可见的。

2.没有可以改变对象的方法。想使它们成为final,或者声明类为final,这样方法就不能被修改器重载.

3.不要泄露可变状态。所有(非私有)方法不能返回任何可被用于修改的内部结构的引用。当你的某个方法调用另一个类的方法时,它不能传递任何这样的引用,

因此被调方法可能在修改中使用引用.

4. 不要忽视构造方法中的this引用。 this 引用,在构造之后相当安全,如果在构造方法中显示了this. 有人可能观察处在不完整状态的对象,runnable 是个内部类,它包含this引用。或者当构造一个事件监听器时,将this引用添加到监听器队列.


将计算并行化之前,先检查 Stream 类库 或 Arrays 类 可能已经有了。


在并行流中使用 lambda 表达式时,远离对共享对象的不安全修改.

Arrays.parallelSetAll 以函数的计算值填充数组,函数接收元素索引并计算该位置的值.


对基本类型值或对象的数组进行排序.

Arrays.parallelSort(words, Comparator.comparing(String::length);

所有方法中,可以提供范围.


ConcurrentHashMap

场景:多个线程遇到单词,想计算单词的频率。如下是不安全的:


 

ConcurrentHashMap<String,Long> map = new ConcurrentHashMap<>();

... 

Long oldValue = map.get(word);

Long newValue = oldValue == null ? 1 : oldValue + 1;

map.put(word, newValue) ; // error -- 可能无法替换 oldValue;

另一个线程可能同时正在更新计数。


 

更新要按如下处理才线程安全

compute 来安全地更新一个值,通过一个key和计算新值的函数来调用compute 方法。

函数接收key 以及关联的值,或如果没有值,就为null, 然后计算新值。

map.compute(word, (k,v) -> v == null ? 1 : v + 1)

compute 方法是原子的- 其他线程在计算进行期间,不能修改映射条目。

computeIfPresent : 已经存在一个旧值。

computeIfAbsent : 尚未存在值.


另一个原子操作: putIfAbsent ,计数器的初始化

map.putIfAbsent(word, 0L);

当一个key 第一次被添加到映射时,要做些特殊的事。使用merge方法会特别方便。它的一个参数用来标识key尚未存在时的初始值。

否则,结合已存值和初始值,调用你提供的函数(不像compute方法,该函数不会处理key)

map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);

map.merge(word,1L, Long::sum);

传递给 compute 和 merge 方法的函数应该是能快速执行完毕的函数,且不应该试图修改映射.


搜索,转换,访问 ConcurrentHashMap 的批量操作,在数据快照上操作,即使其他线程同时操作映射,这些批量操作也可以安全进行.


阻塞队列是在任务之间协调工作的一个常用工具。

生产者任务在队列中插入项,消费者从队列中获取项。

队列让你安全地将数据从一个任务移交到另外一个任务。

队列平衡工作负载的方式:给一个满了的队列添加元素时,或从一个已经空了的队列中删除元素时,操作会阻塞.

根据在队列已满或为空时 阻塞队列方法执行行为的不同,划分为三类:

除了阻塞方法外,一些在不成功执行时抛出异常的方法,

不能完成任务,不是抛出异常,而是返回一个故障指示器的方法。

put  , take 队列满或空,则阻塞。

offer, 添加一个元素并返回true, 队列若满,返回false.  超时变种。

poll 删除并返回头元素,队列若空,返回null.  超时变种。

peek 返回头元素,如果队列为空,返回null;

add, remove, element  满 或 空 或 空, 抛出 IllegaalStateException, NosuchElementException , ~ 异常。


 

LinkedBlockingQueue 基于链表。

ArrayBlockingQueue 圆环数组。


 

ConcurrentSkipListMap : 需要按顺序遍历键,或者如果需要 NavigableMap 接口中添加的某个方法。 ConcurrentSkipListSet 类

CopyOnWriteArrayList 和 CopyOnWriteArraySet 都是线程安全的集合,修改器都复制了一份底层的数组。如果遍历集合的线程远比修改集合的线程多。

----

ConcurrentHashSett 变通-》 Set<String> words = ConcurrentHashMap.newKeySet() 若需要添加相应的值-》

Set<String> words = map.keySet(1L)

words.add("Java")


原子值

atomic 包中,对整数,long和 boolean值,对象引用 和数组操作的源自性。 原子计数器 和 累加器

public static AtomicLong nextNumber = new AtomicLong();

long id = nextNumber.incrementAndGet();

使用 带有lambda表达式的 updateAndGet 来更新变量

或 accumulateAndGet(..,...) 通过一个二元运算符将原子值和传入的参数组合起来。

返回旧值得 getAndUpdate 方法和 getAndAccumulate 方法。

预测程序存在高度竞争,使用 LongAdder 代替 AtomicLong。


临界区

为了避免损毁共享变量,需要确保每次只有一个线程可以计算并设置新值。

必须完整地,没有中断地执行的代码块,称为临界区。可以使用锁来实现临界区。

synchronized 关键字,所有对象都有固有锁。

锁住一块代码。

将方法声明为 synchronized, 方法体就被接收者参数this 锁住。

synchronized 保证可见性,get 调用者会看到通过 set 调用对变量所做的更新.

 


p351 Queue 的 demo

如果队列为空,take 方法会阻塞。

必须在synchronized 方法内部检查队列是否为空;

否则,其他查询是没有意义的-其他线程可能在同一时刻将队列置空.

 

当前线程现在处于非激活状态并放弃了锁。这样其他的线程就可以向队列添加元素。这被称为条件等待。 

wait 方法是 Object 类的方法,涉及与对象关联的锁.

获取锁时被阻塞的线程 与 调用wait 方法的线程有本质的区别。 一旦线程调用wait 方法,它就进入对象的 等待集合

当锁可用时,线程不会变为可运行状态。它依然是非激活状态,

直到其他线程在同一个对象调用了 notifyAll 方法.

 

调用notifyAll() 方法会重新激活 等待集合中的 所有线程。

当线程 从等待集合中 移除时,它们再次变为 可运行状态, 并且调度器会最终 再次激活它们。

那时它们将尝试 重新获得锁。

只要其中一个线程获得了锁,它会从中断的位置,也就是从调用 wait() 方法的地方继续执行.

这时,线程应该再次测试条件。

不保证条件现在时满足的--因为 notifyAll 方法只是给等待线程信号,通知它们此时等待线程需要的条件可能满足了

且值得再次检查一下条件,因此,在循环冲测试

 

notify() 从等待集合中只移除一个线程,如果被选中的线程发现自己依然无法处理,会被再次阻塞,

如果没有其他线程再次调用 notify, 程序就死锁了. 

构建拥有阻塞方法的数据结构,使用 wait 和 notifyAll 方法是合适,不容易。

只是想让线程等待,直到某些条件满足了,要使用JUC 中提供的某个同步类,如 CountdownLatch 和 CyclicBarrier.


线程

线程是实际执行任务的基本单元。最好使用 executor 为你管理线程。

 

静态 sleept 方法使得当前线程睡眠一段时间,这样其他线程有机会运行.

如果向等待一个线程完成。调用 join 完成

 

线程的 run 方法正常或 因为抛出异常 返回时,线程结束。

调用线程的 未捕获异常处理器。

创建线程时,处理器被设置为线程组的 未捕获异常处理器,它时最终的全局处理器,调度器的处理。

通过调用 setUncaughtExceptionHandler 设置。


Java 中,任务取消是合作式的.

每个线程都有中断状态。表示取消请求。

Runnable task = () ->{

  while(更多判断工作){ 

    if ( Thread.currentThread().isInterrupted()) return ;

    // do work

  }

};

Thread.interrupted 静态方法,获取当前线程的中断状态,然后清除状态,并返回旧状态。


线程临时变得不活跃,如果线程等待另一个线程计算的值,输入/输出 或它睡眠了以给其他线程运行的机会时,会发生这种情况。

当线程等待 或 睡眠时 被中断了,它立即被激活 - 此情况下,中断状态还未被设置。相反,抛出 IterruptedException.

该检查异常必须在 Runnable 的 run 方法中被捕获。

无需检查中断状态,

如果线程在 Thread.sleep 调用之外被中断了,会设置中断状态且一调用 Thread.sleep ,抛出异常 IntteruptedException.

要么在 catch 设置一下中断状态 

catch ( InterruptedException ex ) { 

  Thread.currentThread().interrupt();

良好实践: method sign  传播到能够处理该异常的程序处理器。


线程变量

ThreadLocal 辅助类: 通过给每个线程一个自己的实例,避免共享。


 

线程可以按组收集,executor 管理任务组,

线程状态: 新建/正在运行/ 阻塞在输入/输出 / 等待 / 终止状态。

线程的异常,传递给线程的未捕获异常处理器,默认,它的堆栈踪迹会被转储到 System.err, 可以安装自己的处理器。

后台线程 daemon, 发送报时信号, 清除陈旧缓存项,后台线程是有用的。 thead.setDaemon(true);


 

 

 异步计算

UI回调中的长期运行任务

 

必须要小心这样一个线程的所作所为。UI, 例如 JavaFX, Swing, Android 都不是线程安全的。

如果从多个线程控制UI元素,会损坏。 会被抛出异常。

需要将 UI 更新安排在 UI 线程。

UI类库提供了一些安排 Runnable 在 UI 线程执行的机制: JavaFX

Platform.runLater ( () -> messaage.appendText( line + "\n" ));

UI 类库通常提供某种辅助类管理细节: 给用户反馈进度。

Swing SwingWorker 和 Android 中的 AsyncTask.

指定长期运行任务(在单独的线程中运行)的行为,以及进度更新和最终的位置(在UI线程中运行)

JavaFX 的 Task 类  


CompletableFuture

传统的处理非阻塞调用的方法是事件处理器,程序员为了某种在任务完成后应该发生的行为注册处理器。

如果下一个行为也是异步的,则之后的下一个行为在不同的事件处理器中。

程序逻辑分散在不同程序处理函数中,还要考虑错误处理。

试图在一组事件处理器中实现这样的控制流,难以理解.


 

给 CompletableFuture<T> 对象添加一个 Action

thenApply      T -> U               在结果上引用一个function

thenCompose     T -> CompletableFuture<U>      对结果调用一个Func并执行返回的future对象

handle            (T,Throwable) -> U           处理结果或错误。

thenAccept      T -> void                类似 thenApply ,但结果为 void 类型

whenComplete     (T, Throwable) -> void         与 handle 类似,但结果为void 类型

thenRun          Runnable                执行返回 void 的 Runnable 对象.


 

thenCombine

thenAcceptBoth

runAfterBoth

applyToEither

acceptEither

runAfterEither

static allOf

static anyOf


进程

ProcessBuilder -> Process : 在一个单独的操作系统进程中执行命令,让你可以和标准输入流,输出流,错误流 交互。

List<String>  或 可执行的命令字符

process .... start();

int result = process.waitFor()  , 超时设置

int result = p.existValue()    0 成功, 非0 错误。

p.destroyForcibly(); 

Unix  SIGTERN  ,  SIGKILL


 

posted @ 2021-02-16 10:58  君子之行  阅读(147)  评论(0)    收藏  举报