Java 线程总结(三) —— 线程的基本协作机制
1、Java 中多线程协作的基本机制 wait/notify。
2、常见的线程协作场景:
- 生产者/消费者协作模式
- 同时开始
- 等待结束
- 异步结果
- 集合点
3、Java 在 Object 类中,定义了一些线程协作的基本方法,使得每个对象都可以调用这些方法,这些方法有两类,一类是 wait,另一类是 notify。
4、wait() 方法的原理?
每个对象都有一把锁和一个锁等待队列,一个线程在进入 synchronized 代码块时,会尝试获取锁,获取不到的话会把当前线程加入到锁等待队列中。除了用于锁的等待队列,每个对象还有另一个等待队列,表示条件队列,该队列用于线程间的协作。调用 wait 就会把当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,它需要等待一个条件,这个条件它自己改变不了,需要其他线程改变。当其他线程改变了条件后,应该调用 Object 的 notify 方法。wait 虽然是在 synchronzied 方法内,但调用 wait 时,线程会释放对象锁。 而 notify 做的事情就是从条件队列中选一个线程,将其从队列中移除并唤醒,notifyAll 和 notify 的区别是,它会移除条件队列中所有的线程并全部唤醒。
wait 的具体过程是:
1.把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态变为 WAITING 或 TIMED_WAITING。
2.等待时间到或被其他线程调用 notify/notifyAll 从条件队列中移除,这时,要重新竞争对象锁。
- 如果能够获得锁,线程状态变为 RUNNABLE,并从 wait 调用中返回。
- 否则,该线程加入对象锁等待队列,线程状态变为 BLOCKED,只有在获得锁后才会从 wait 调用中返回。
3.线程从 wait 调用中返回后,不代表其等待的条件就一定成立了,它需要重新检查其等待的条件。
1 | public class WaitThread extends Thread{ |
5、notify() 方法的原理?
notify 做的事情就是从条件队列中选一个线程,将其从队列中移除并唤醒。调用 notify 会把在条件队列中等待的线程唤醒并从队列中移除,但它「这里指主线程」不会释放对象锁,也就是说,只有在包含 notify 的 synchronzied 代码块执行完后,等待的线程才会从 wait 调用中返回。
6、wait/notify 方法只能在 synchronized 代码块内被调用,如果调用 wait/notify 方法时,当前线程没有持有对象锁,会抛出异常 java.lang.IllegalMonitorStateException。
那么问题来了:为什么wait(),notify(),notifyAll() 必须在同步方法/代码块中调用?
因为 wait() 和 notify() 之间的竞态条件导致必须在同步方法/代码块中调用,因为 wait 和 notify 都需要访问和操作对象的等待队列,wait 入队,notify 出队,它们对等待队列的访问顺序是敏感的,所以需要放到同步代码中,以避免竞态条件问题。
在 Java 中,所有对象都能够被作为「监视器即 Monitor」,而监视器是指:一个拥有 一个独占锁,一个入口队列和一个等待队列的 实体。对于对象的同步方法来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们。eg:一个同步方法是独占的。如果在线程调用某一对象的同步方法时,对象的独占锁被其他线程拥有,那么当前线程将处于阻塞状态,并添加到对象的入口队列中。
当一个线程正在某一个对象的同步方法中运行时调用了这个对象的 wait() 方法,那么这个线程将释放该对象的独占锁并被放入这个对象的等待队列。注意,wait() 方法强制当前线程释放对象锁。这意味着在调用某对象的 wait() 方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的 wait() 方法。
当某线程调用某对象的 notify() 或 notifyAll() 方法时,任意一个或者所有在该对象的等待队列中的线程,将被转移到该对象的入口队列。
调用 wait() 方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用 notify()或notifyAll() 方法的原因通常是,调用线程希望告诉其他等待中的线程「特殊状态已经被设置」。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
假设 wait(),notify(),notifyAll() 方法不需要加锁就能够被调用。此时消费者线程调用 wait() 正在进入状态变量的等待队列(可能还未进入)。在同一时刻,生产者线程调用 notify() 方法打算向消费者线程通知状态改变。那么此时消费者线程将错过这个通知并一直阻塞「因为该线程还没有进入到等待队列中,但是 notify 开始检查等待队列是否有线程了,检查发现没有,notify 失效,这时该线程进入到了等待队列,可能永远错过被唤醒的机会」。因此,对象的 wait(),notify(),notifyAll() 方法必须在该对象的同步方法或同步代码块中被互斥地调用。
简单总结一下,wait/notify 方法看上去很简单,但往往难以理解 wait 等的到底是什么,而 notify 通知的又是什么,我们需要知道,它们与一个共享的条件变量有关,这个条件变量是程序自己维护的,当条件不成立时,线程调用 wait 进入条件等待队列,另一个线程修改了条件变量后调用 notify,调用 wait 的线程唤醒后需要重新检查条件变量。从多线程的角度看,它们围绕共享变量进行协作,从调用 wait 的线程角度看,它阻塞等待一个条件的成立。 我们在设计多线程协作时,需要想清楚协作的共享变量和条件是什么,这是协作的核心。
上面的代码的共享变量是 WaitThread 对象的 fire 变量,结束等待的条件是 fire 的值为 true,这是面代码协作的核心。但是,协作的核心虽然是 fire 变量,但是作用的却是 WaitThread 整个对象,是通过这个对象的条件队列进行协作的,而不是这个对象的某个变量。
7、Java 中每个对象只能有一个条件等待队列,这是 Java wait/notify 机制的局限性,这使得对于等待条件的分析变得复杂。
8、Java 提供了专门的阻塞队列实现,包括:
- 接口 BlockingQueue 和 BlockingDeque
- 基于数组的实现类 ArrayBlockingQueue
- 基于链表的实现类 LinkedBlockingQueue 和 LinkedBlockingDeque
- 基于堆的实现类 PriorityBlockingQueue
9、Java 中线程间协作的基本机制 wait/notify,协作关键要想清楚协作的共享变量和条件是什么。
参考博客
Java 编程的逻辑 - 线程的基本协作机制 (上)
Java 编程的逻辑 - 线程的基本协作机制 (下)
为什么 wait(),notify(),notifyAll() 必须在同步方法/代码块中调用?
为什么 wait 和 notify 必须在同步方法或同步块中调用