Java多线程4

120 阅读7分钟
原文链接: mp.weixin.qq.com

多线程编程中分而治之的两种方式:

基于数据的分割、基于任务的分割

设置线程数:线程数不能过小,不然可能导致无法充分利用处理器资源;线程数不能过大,会增加上下文切换以及其他开销

Amdahl’s定律: S=1/P   (P为整个计算中串行部分的耗时比率)

 

线程间协作:

1、wait/notify

在单线程中,if语句的条件不成立则括号里面的内容不会被执行,但在多线程中则不一样了;当条件不成立时,使用wait方法使当前线程暂停;其他线程修改条件变量的值后,再使用notify方法唤醒;此时原本不会被执行的代码也会被执行了。

Object.wait()的代码实现:

Synchroized(someObjcct){

       While(保护条件不成立) {

       SomeObject.wait();

}

doAction();

}

上述模板方法被称为受保护方法,其三要素:保护条件,暂停当前线程,目标动作

由于一个对象的同一个方法可以被多个线程执行,所以一个对象可能存在多个等待线程。上面someObject的wati()方法可以通过其他线程执行someObject.notify()方法来唤醒。someObject.wait()会以原子操作的方式使其执行线程暂停并使该线程释放其持有的内部锁。当前线程被暂停时其对wait()的调用并未返回。

 

Object.notify()的伪代码实现:

Synchronized(someObject){

        updateSharedState();//更新等待线程的保护条件涉及的共享变量

        someObject.notify();

}

上述模板方法被称为通知方法,包括两要素:更新共享变量、唤醒其他线程

调用Object.notify()方法所唤醒的线程仅是相应对象上的一个任意等待线程,这个被唤醒的线程可能不是我们真正想要唤醒的线程;因此我们有时会借助Object.notifyAll();它可以唤醒相应对象上的所有等待线程。

Object.wait(long)允许我们指定一个超时时间,如果被暂停的等待线程在这个时间内没有被其他线程唤醒,java虚拟机会自动唤醒该线程。

 

wait/nofity开销及问题

1、过早唤醒:notifyAll会唤醒同一对象上的所有其他wait状态线程。可以使用Condition接口来解决

2、信号丢失:如果等待线程在执行wait()前没有先判断保护条件是否已成立,那么通知线程在该等待线程进入临界区之前就已经更新了共享变量,使得相应的保护条件成立并进行了通知,但此时等待线程还没有被暂停,导致后面被wait时,没有其他线程通知,使得一直处于wait状态。解决办法:Object.wait调用放在一个循环语句中就能避免信号丢失

3、欺骗性唤醒:等待线程也可能在没有其他任何线程执行Object.notify/notifyAll的情况下被唤醒。解决办法:将受保护条件的判断和Object.wait调用放在循环语句中。

4、上下文切换问题

 

Thread.join()

是当前线程等待目标线程结束后才继续运行

Thread.join(long)

指定一个超时时间,如果目标线程在指定时间内没有终止,那么当前线程也会继续运行

 

java条件变量:Condition接口

可作为wait/notify的替代,解决过早唤醒问题

await()  == wait

signal() == notify

signalAll() == notifyAll

每个Condition实例内部维护了一个用于存储等待线程的队列,这样每个实例调用await时会存入自己的等待队列中,当调用signal或signalAll时会使自己实例中的等待队列中的线程被唤醒

 

倒计时协调器:CountDownLatch

用来实现一个或多个线程等待其他线程完成一组特定的操作之后才继续运行;这组操作被称为先决条件

 

栅栏:CyclicBarrier

多个线程可能需要互相等待对方执行到代码中的某个地方,这时这些代码才能够继续执行

 

阻塞队列:BlockingQueue

从传输通道中存入一个产品或者取出一个产品时,相应的线程可能因为传输通道中没有产品或者其存储空间已满而被阻塞(暂停)

常见的阻塞方法操作:InputStream.read(),ReentrantLock.lock(),申请内部锁等

BlockingQueue的实现类:

ArrayBlockingQueue: put、take操作时不会增加垃圾回收负担,但使用的是同一个显示锁,从而导致锁的高争用及上下文切换   适合并发程度较低

LinkedBlockingQueue: 可能会增加垃圾回收负担,put、take操作使用的是两个锁,但维护长度时使用的原子变量可能会被争用   适合并发程度较大

SynchronizedQueue: put \take操作要成对出现才会继续     消费者与生产者处理能力相差不大的情况下使用

 

限购:流量控制与信号量 Semaphore

所访问的特定资源或者执行特定操作的机会统一叫做虚拟资源,Semaphore相当于虚拟资源配额管理器

Semaphore.acquire/release分别用于申请配额和返还配额;申请的配额不足时,线程暂停。

 

管道:PipedOutStream/PipedInStream 线程间的输入与输出

适合在两线程间使用,即单生产者-单消费者   数据是字节形式

 

双缓冲Buffering与Exchanger

Exchanger相当于一个只有两个参与方的CyclicBarrier; Exchanger.exchange(V)相当于CyclicBarrier.await()

消费者线程向生产者线程提供空的缓冲去,生产者线程向消费者线程提供一个已经填充完毕的缓冲区;即一手交钱一手交货

 

线程中断机制

Thread.currentThread().isInterrupted()获取该线程的中断标记值也可以通过

Thread.interrupted()来获取并重置中断标记值

目标线程对中断的响应:

a、        无影响:无法对目标线程进行中断响应InputStream.read()、ReentrantLock.lock()、申请内部锁等阻塞操作

b、        取消任务的运行

c、         工作者线程停止:target.interrupt()使target终止,状态变为TERMINATED

 

InterruptedException异常处理及中断响应

对InterruptedException等异常进行处理的方式:

a、        不捕获InterruptedException 其实际是将中断处理抛给上层代码

b、        捕获InterruptedException后重新将异常抛出应用层需要捕获InterruptedException并对此做一些中间处理,再将难题抛给上层代码

c、         捕获InterruptedException并在捕获该异常后中断当前线程,这种策略是捕获异常后又恢复中断标志,相当于当前代码告诉其他代码:发现了中断但不知道怎么处理,但保留了中断标志等你处理

比较危险的一种处理方式是“吞没” 即既不抛出也不保留标志

 

线程停止

a、        服务或者系统关闭:由于非守护线程会阻止java虚拟机正常关闭,因此在系统停止前所有用户线程都应该先行停止

b、        错误处理:同质线程中的一个线程出现不可恢复异常时,其他线程没有必要继续运行下去了,我们需要主动停止其他工作者线程

c、         用户取消任务

从通用性角度看,线程中断标志不能做为线程停止标记,因为线程中断标志可能会被目标线程清空。需要一个专门的实例变量,但还不够、因为目标线程可能因为执行了一些阻塞方法而被暂停,所以我们可能还需要给目标线程发送中断以将其唤醒。

生产者-消费者模式中的线程停止

单生产者-单消费者模式中:生产者线程在其终止前往传输通道中存入一个特殊产品作为消费者线程的停止标志,消费者线程取出这个停止标志后退出run方法

但在多生产者线程多消费者线程间使用会比较困难,此时使用原子实例变量及volatile线程停止标记来处理。