java多线程编程基础三-线程协作

wait/notify(通知与唤醒)

Object.wait()/Object.wait(long):是执行线程暂停(生命周期状态变为WAITING)
Object.notify()/Object.notifyAll():唤醒被暂停的线程

等待线程和通知线程必须调用同一个对象的wait方法、notfiy方法来实现等待和通知。
调用一个对象的notify方法所唤醒的线程仅是该对象上的一个任意等待线程。
notify方法调用应该尽可能地放在靠近临界区结束的地方。

wait/notify面临的问题:

  1. 过早唤醒:在线程还不满足保护条件的时候唤醒线程
  2. 信号丢失:在线程调用wait方法之前没有判断保护条件,造成通知线程在进入临界区之前就更新了相关共享变量,满足条件直接进行通知。等待线程失去接收通知,一直处于等待状态。
  3. 欺骗性唤醒:在其他线程未调用notify或notifyAll方法时被唤醒。
  4. 上下文切换

Object.notify()

  1. 唤醒的是其所属对象上的任意一个等待线程,并且Object.notify()本身在唤醒线程时是不考虑保护条件的。
  2. 可能导致信号丢失

Object.notifyAll()

  1. 方法唤醒的是其所属对象上的所有等待线程。
  2. 效率不高(1照成)

使用Object.notify()替换Object.notifyAll()时需要满足的条件:

  1. 一次通知仅需唤醒至多一个线程
  2. 相应对象的等待集中仅包含同质等待线程。
    同质等待线程:指这些线程使用同样的保护条件,并且这些线程在调用wait方法后的执行逻辑一致。
    典型的同质线程:使用一个Runnable接口创建的不同线程,或者从同一个Thread子类中new出来的多个实例

Thread.join():使当前线程等待目标线程完成后继续进行
Thread.join(long):当目标线程在指定的时间内没有完成时,当前线程依然继续执行。

java条件变量

java.util.concurrent.locks.Condition接口,可以代替wait/notify实现等待/通知,为解决过早唤醒问题提供了支持;并解决了Object.wait(long)不能区分返回是否由等待超时而导致的问题(使用Condition.awaitUntil(date),返回ture即为等待未超时)

Condition接口本身只是对解决过早唤醒问题提供了支持。要真正解决过早唤醒问题,我们需要通过应用代码维护保护条件与条件变量之间的对应关系,即使用不同的保护条件的等待线程需要调用不同的条件变量的await 方法来实现其等待,并使通知线程在更新了相关共享变量之后,仅调用与这些共享变量有关的保护条件所对应的条件变量的signal/signalAll 方法来实现通知。

倒计时协调器:CountDownLatch
CountDownLatch可以实现一个(或者多个)线程,等待其他线程完成一组特定的操作之后才继续执行。这组操作被称为“先决操作”

CountDownLatch 内部会维护一个用于表示未完成的先决操作数量的计数器。CountDownLatch.countDown()每被执行一次就会使相应实例的计数器值减少l。CountDownLatch. await()相当于一个受保护方法,其保护条件为“计数器值为0”(代表所有先决操作巳执行完毕),目标操作是一个空操作。因此,当计数器值不为0时CountDownLatch.await()的执行线程会被暂停,这些线程就被称为相应CountDownLatch上的等待线程。CountDownLatch.countDown()相当于一个通知方法,它会在计数器值达到0的时候唤醒相应实例上的所有等待线程。

栅栏(CyclicBarrier):
有时候多个线程可能需要相互等待对方执行到代码中的某个地方(集合点),这时这些钱程才能够继续执行。CyclicBarrier就实现了这种等待。

使用CyclicBarrier实现等待的线程被称为参与方(Party)。参与方只需要执行CyclicBarrier.await()就可以实现等待。尽管从应用代码的角度来看,参与方是并发执行CyclicBarrier.await()的,但是,CyclicBarrier内部维护了一个显式锁,这使得其总是可以在所有参与方中区分出一个最后执行CyclicBarrier.await()的线程,该线程被称为最后一个线程。除最后一个钱程外的任何参与方执行CyclicBarrier.await()都会导致该线程被暂停(线程生命周期状态变为WAITING)。最后一个线程执行CyclicBarrier.await()会使得使用相应CyclicBarrier实例的其他所有参与方被唤醒,而最后一个线程自身并不会被暂停。

CyclicBarrier内部使用了一个条件变量trip来实现等待/通知。CyclicBarrier内部实现使用了分代(Generation)的概念用于表示CyclicBarrier实例是可以重复使用的。除最后一个线程外的任何一个参与方都相当于一个等待线程,这些线程所使用的保护条件是“当前分代内,尚未执行await方法的参与方个数(parties)为0”。当前分代的初始状态是parties等于参与方总数(通过构造器中的parties参数指定)。CyclicBarrier.await()每被执行一次会使相应实例的parties值减少l。最后一个线程相当于通知线程,它执行CyclicBarrier.await()会使相应实例的parties值变为0,此时该线程会先执行barrierAction.run(),然后再执行trip.signalAll()来唤醒所有等待线程。接着,开始下一个分代,即使得CyclicBarrier的parties值又重新恢复为其初始值。

信号量(Semaphore)
JDK1.5中引人的标准库类java.util.concurrent.Semaphore可以用来实现流量控制。
Semaphore相当于虚拟资源配额管理器,它可以用来控制同一时间内对虚拟资源的访问次数。为了对虚拟资源的访问进行流量控制,我们必须使相应代码只有在获得相应配额的情况下才能够访问这些资源。为此,相应代码
在访问虚拟资源前必须先申请相应的配额,并在资源访问结束后返还相应的配额。Semaphore.acquire()/release()分别用于申请配额和返还配额。Semaphore.acquire()在成功获得一个配额后会立即返回。如果当前的可用配额不足,那么Semaphore.acquire()会使其执行线程暂停。Semaphore内部会维护一个等待队列用于存储这些被暂停的钱程。Semaphore.acquire()在其返回之前总是会将当前的可用配额减少l。Semaphore.release()会使当前可用配额增加l,井唤醒相应Semaphore实例的等待队列中的一个任意等待线程。

管道:线程间的直接输出与输入
Java 标准库类PipedOutputStream 和PipedInputStream是生产者一消费者模式的一个具体例子。PipedOutputStream和PipedlnputStream分别是OutputStream和InputStream的一个子类,它们可用来实现线程间的直接输出和输入。所谓“直接”是指从应用代码的角度来看,一个线程的输出可作为另外一个线程的输入,而不必借用文件、数据库、网络连接等其他数据交换中介。
PipedOutputStream相当于生产者,其生产的产品是字节形式的数据;PipedlnputStream相当于消费者。PipedlnputStream内部使用byte型数组维护了一个循环缓冲区(CircularBuffer),这个缓冲区相当于传输通道。在使用PipedOutputStream 、PipedInputStream进行输出、输入操作前,PipedOutputStream实例和PipedInputStream实例需要建立起关联(Connect)。建立关联的PipedOutputStream 实例和PipedInputStream 实例就像一条输送水流的管道,管道的一端连着注水口(PipedOutputStream),另一端连着出水口(PipedInputStream)。这样,生产者所生产的数据(相当于水流)通过向PipedOutputStream实例输出(相当于向管道注水),就可以被消费者通过关联的PipedInputStream实例所读取(相当于从出水口接水)。PipedOutputStream 实例和PipedInputStream 实例之间的关联可以通过调用各自实例的connect方法实现,也可以通过在创建相应实例的时候将对方的实例指定为自己的构造器参数来实现。

PipedOutputStream 和PipedlnputStream 适合在单生产者一单消费者模式中使用。生产者线程发生异常而导致其无法继续提供新的数据时,生产者线程必须主动提前关闭相应的PipedOutputStream实例(调用PipedOutputStream.close())。

双缓冲和Exchange:
缓冲(Buffering)是一种常用的数据传递技术。缓冲区相当于数据源(Source,即数据的原始提供方)与数据使用方(Sink)之间的数据容器。从这个角度来看,数据源相当于生产者,数据使用方相当于消费者。数据源所提供的数据相当于产品,而缓冲区可被看作产品的容器或者外包装。在多线程环境下,有时候我们会使用两个(或者更多)缓冲区来实现数据从数据源到数据使用方的移动。其中一个缓冲区填充满来自数据源的数据后可以被数据使用方进行“消费”,而另外一个空的(或者已经使用过的)缓冲区则用来填充来自数据源的新的数据。这里,负责填充缓冲区的是一个线程(生产者线程),而使用已填充完毕的另外一个缓冲区的则是另外一个线程(消费者线程)。因此,当消费者线程消费一个已填充的缓冲区时,另外一个缓冲区可以由生产者线程进行填充,从而实现了数据生成与消费的并发。这种缓冲技术就被称为双缓冲( Double Buffering )。

JDKl.5中引人的标准库类java.util.concurrent.Exchanger 可以用来实现双缓冲。
Exchanger相当于一个只有两个参与方的CyclicBarrier。Exchanger.exchange(V)相当于CyclicBarrier.await()。

线程中断机制:Thread.interrupted()
一个线程请求另外一个线程停止其当前正在执行的任务。中断只是一个请求,该请求是否会被满足,取决于目标线程的处理。

线程停止:线程停止的场景:系统或服务关闭、错误处理、用户取消
java并没有提供直接停止线程的接口。(Thread.stop()已经被废弃)

你可能感兴趣的:(java多线程编程基础三-线程协作)