接上一篇:《java并发系列(2)——线程共享,synchronized与volatile》
文章目录
2.4 线程协作/通信
2.4.1 wait/notify
首先要注意的是:wait,notify,notifyAll 这三个方法跟 sleep,yield,join,interrupt,suspend,resume,stop 这些不一样,后者是 Thread 特有的方法,前者是 Object 的方法。
2.4.1.1 monitor 回顾
在 wait,notify 之前,先回顾一下 monitor。前面在讲 synchronized 时提到 monitor 计数器,以及 monitor 上标记的当前持有锁的线程。这里会再增加一个 wait set。
monitor 持有的信息:
- 计数器:获得锁或重入锁时,计数器 +1,退出时计数器 -1,计数器为 0 意味着锁没有被任何线程获得;
- 线程:记录了当前锁被哪个线程所持有;
- wait set:调用了 wait 方法的线程,会被记录在这里。(每个 Object 都有自己的 monitor,在哪个 Object 上调用 wait,就会被记录到哪个 Object 的 monitor 的 wait set。)
2.4.1.2 wait/notify 的作用
概况地说,wait 方法会使当前线程进入阻塞状态,直到被其它线程使用 notify 或 notifyAll 唤醒。
具体行为如下:
wait:
- 进入 wait 时:线程释放 monitor 锁,进入阻塞状态,线程被记录到 wait set;
- 被 notify 时:线程从 wait set 中被清除,进入就绪状态等待线程调度,获得 cpu 使用权开始执行后先竞争 monitor 锁(行为与 synchronized 一样,并且不会比其它线程更优先竞争到锁),所有同步状态恢复到调用 wait 方法之前,然后从 wait 方法正常返回;
- 被 interrupt 时:线程行为与被 notify 相同,区别是从 wait 方法异常返回(同时 interrupt 状态会被清除);
- 意外醒来:wait 中的线程有极低的概率在没有超时,没有被 notify,没有被 interrupt 的情况下醒来,这是操作系统导致的,被称为“欺骗性唤醒”。
notify:如果 wait set 中有线程,唤醒 wait set 中的一个线程(具体唤醒哪个线程不可控,JVM 可自由实现);如果 wait set 中没有线程就忽略。
notifyAll:唤醒 wait set 中所有的线程。
2.4.1.3 wait/notify 的标准使用范式
//wait
synchronized (object) {
while (<condition>) {
object.wait();
//object.wait(timeout);
}
//...
}
//notify
synchronized (object) {
//...
object.notify();
//object.notifyAll();
}
- wait,notify,notifyAll 方法都需要先获得锁(在哪个对象上执行方法,就需要获得哪个对象的锁);
- wait 方法要放在循环里面执行(当被唤醒时检查是否满足放行条件,不满足就继续 wait),防止被意外唤醒;
- notify 方法尽量在最后执行(要保证 notify 执行完后尽快释放锁),因为 wait 线程被唤醒后需要获得锁,notify 线程如果不释放锁,即使唤醒了 wait 线程,wait 线程也还是会阻塞。
wait,notify 示例代码见《利用wait/notify模拟消息队列》
2.4.1.4 wait,sleep,yield
三者异同:
- 都会导致线程阻塞(如果 yield 没有被线程调度器忽略);
- wait 和 sleep 都能被 interrupt;
- wait 会释放锁,sleep 和 yield 不会(顺便一提,suspend 也不会);
- wait 除了超时苏醒,还能被唤醒,sleep 只能超时苏醒;
- wait 由于必须先获得锁,因此会伴随着线程工作内存的刷新,而 sleep 和 yield 都没有规定必须刷新内存。
2.4.1.5 wait 阻塞,sleep 阻塞与 synchronized 阻塞
从操作系统层面讲,都是阻塞状态,阻塞状态的线程都不会被调度。
在 Java 层面,wait 对应的线程状态是 Thread.State.WAITING 或者 Thread.State.TIMED_WAITING,取决于是否设置了超时。
sleep 阻塞对应的线程状态是 Thread.State.TIMED_WAITING,因为 sleep 必须设置超时时间。
synchronized 阻塞对应的线程状态是 Thread.State.BLOCKED,这个状态的阻塞是因为锁竞争而导致的。
所以,wait 可能会产生两种阻塞状态,在醒来之前是 WAITING 或 TIMED_WAITING,醒来之后会竞争锁,如果没有竞争到锁则可能会进入 BLOCKED 状态。
另外,synchronized 未必会导致线程阻塞,即使没有竞争到锁。Java 为了减少线程切换开销,在没有竞争到锁的情况下有可能让线程空跑(自旋状态,类似于跑一个空的死循环)而非进入阻塞状态。
2.4.1.6 notify 与 notifyAll
使用 notify 不会出现问题的情况下尽量使用 notify,而不是 notifyAll。
因为 notifyAll 会唤醒多个线程,唤醒的线程都要竞争锁,必然只有一个线程得到锁,其它被唤醒的线程又会重新进入阻塞状态,增加了不必要的线程切换开销。
适合使用 notify 的情况:
Thread a = new Thread(() -> {
synchronized (monitor) {
while (conditionA()) {
monitor.wait();
}
System.out.println("A");
}
});
Thread b = new Thread(() -> {
synchronized (monitor) {
while (conditionA()) {
monitor.wait();
}
System.out.println("A");
}
});
这两个线程,醒来之后做的事情是一样的,唤醒谁都无所谓,使用 notify 就可以。
如果再加一个这样的线程:
Thread c = new Thread(() -> {
synchronized (monitor) {
while (conditionB()) {
monitor.wait();
}
System.out.println("B");
}
});
这个线程醒来后干的事情不一样,那么使用 notify 唤醒的线程可能不符合预期,这时候就只能用 notifyAll,不符合预期的线程因为不满足退出 while 循环条件会重新 wait。
当然,如果不在乎这点线程切换开销,反而更担心代码运行出错,那么全都 notifyAll 也不是不行。
2.4.2 join
join 方法的作用可以概括为“线程插队”,即阻塞当前线程,让其它线程先执行,等其它线程执行完或超时,当前线程再恢复执行。
join 方法是用 wait 方法实现的,所以 wait 方法可以被 interrupt,join 方法自然也可以。
当然 join 方法也必须获得线程对象的锁。
可以看下它的实现:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
核心逻辑也就是下面这三行代码(else 里面增加了对超时时间的控制,本质一样):
while (isAlive()) {
wait(0);
}
wait(0) 等同于 wait() ,即没有超时时间。
这三行代码的意思就是: 如果 join 进来的线程还活着(即已经调了 start 方法但还没有执行完成),就一直等。
不用担心这里的 wait 会永远等待下去,因为线程终止的时候,会调用 this.notifyAll 方法。
可以写几行代码测试一下:
package per.lvjc.concurrent.waitnotify;
import java.util.concurrent.TimeUnit;
public class ThreadTerminatedTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("thread begin");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread end");
});
thread.start();
synchronized (thread) {
thread.wait();
}
System.out.println("main end");
}
}
在 Thread 对象上 wait。跑一下可以发现 thread 执行完之后,wait 是会被唤醒的。