Fork me on GitHub

Java 线程总结(三)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class WaitThread extends Thread{
// 协作的条件变量是 fire
private volatile boolean fire = false;

@Override
public void run() {
try {
synchronized (this) {
while (!fire) {
System.out.println("wait......before");
wait();
System.out.println("wait......after");
}
}
System.out.println("fired");
} catch (InterruptedException e) {
}
}

// 两个线程都要访问协作的变量 fire,容易出现竞态条件,所以相关代码都需要被 synchronized 保护
public synchronized void fire() {
this.fire = true;
notify();
}

public static void main(String[] args) throws InterruptedException {
WaitThread t = new WaitThread();
/**
* 线程启动以后,开始了 run 方法的执行,首先获取 WaitThread 的对象锁
* 然后执行 while 循环,由于「fire = false」,所以进入 while 循环体
* 执行 wait 方法,即放弃对象锁,并把单前线程 t 加入到条件等待队列中,然后线程阻塞在了 wait 方法这,不再向下执行
*/
t.start();
Thread.sleep(1000);
System.out.println("fire");
/**
* 主线程执行 fire 方法,干了两件事:把 fire 设置为 true,调用了 WaitThread 对象的 notify 方法
* 一旦调用 notify 方法,等待的 t 线程就被唤醒了,并且把 t 线程从条件等待队列中移除
* 接下来,t 线程要尝试去获取 WaitThread 对象的锁,但是现在这把锁被主线程在 fire 方法中占用了,只有等到主线程释放它才可以获得到
* 由于 t 获取锁没有获取到,所以 t 线程加入到了 WaitThread 对象的锁等待队列中了,挪坑了!
* 当主线程释放锁以后,t 线程终于抢到了锁,然后 t 线程从 wait 调用中返回了,开始执行 wait 调用下面的语句 「System.out.println("wait......after");」
* 然后开始执行下一轮 while 循环,发现条件变了,所以不再进入 while 循环,直接执行「System.out.println("fired");」,然后 t 线程结束使命
*/
t.fire();
}
}

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 必须在同步方法或同步块中调用

-------------本文结束感谢您的阅读-------------

本文标题:Java 线程总结(三)

文章作者:Yan ChongSheng

发布时间:2018年08月16日

最后更新:2018年08月29日

原始链接:yanchongsheng.github.io/2018/08/16/Java-Java-Thread-2018-08-16-Java线程总结-三/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

开启打赏模式