接上一篇:《java并发系列(2)——线程共享,synchronized与volatile》
首先要注意的是:wait,notify,notifyAll 这三个方法跟 sleep,yield,join,interrupt,suspend,resume,stop 这些不一样,后者是 Thread 特有的方法,前者是 Object 的方法。
在 wait,notify 之前,先回顾一下 monitor。前面在讲 synchronized 时提到 monitor 计数器,以及 monitor 上标记的当前持有锁的线程。这里会再增加一个 wait set。
monitor 持有的信息:
概况地说,wait 方法会使当前线程进入阻塞状态,直到被其它线程使用 notify 或 notifyAll 唤醒。
具体行为如下:
wait:
notify:如果 wait set 中有线程,唤醒 wait set 中的一个线程(具体唤醒哪个线程不可控,JVM 可自由实现);如果 wait set 中没有线程就忽略。
notifyAll:唤醒 wait set 中所有的线程。
//wait
synchronized (object) {
while (<condition>) {
object.wait();
//object.wait(timeout);
}
//...
}
//notify
synchronized (object) {
//...
object.notify();
//object.notifyAll();
}
wait,notify 示例代码见《利用wait/notify模拟消息队列》
三者异同:
从操作系统层面讲,都是阻塞状态,阻塞状态的线程都不会被调度。
在 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 为了减少线程切换开销,在没有竞争到锁的情况下有可能让线程空跑(自旋状态,类似于跑一个空的死循环)而非进入阻塞状态。
使用 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 也不是不行。
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 是会被唤醒的。