一个线程需要等另一个线程准备好数据时,可以不断检查标记,也可以先等待,等数据就绪后再被唤醒。后一种方式不用一直占着 CPU。
wait 和 notifyAll
下面用一个标记表示数据已经准备好:
public class WaitNotifyDemo {
private final Object lock = new Object();
private boolean ready;
private int value;
public void receive() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait();
}
System.out.println(value);
}
}
public void send(int value) {
synchronized (lock) {
this.value = value;
ready = true;
lock.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyDemo demo = new WaitNotifyDemo();
Thread receiver = new Thread(() -> {
try {
demo.receive();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
receiver.start();
demo.send(42);
receiver.join();
}
}输出 42。即使 send() 先执行,接收线程也会发现 ready 已经为 true,直接读取数据,不会错过通知后一直等待。
调用 wait()、notify()、notifyAll() 时,必须持有对应对象的监视器锁,否则会抛出 IllegalMonitorStateException。wait() 会释放这把锁;收到通知后,还要重新竞争锁,才能从 wait() 返回。
通知不等于条件已经满足。线程可能被虚假唤醒,也可能在重新取得锁之前,条件又被其他线程改变。因此这里用 while 重新检查,而不是 if。
notify() 任意选择一个等待者,notifyAll() 唤醒所有等待者。多个条件共用同一个对象的等待集合时,随意选择一个线程可能唤醒了不该继续的那一类,使用 notifyAll() 后各自检查条件更容易写对。
Condition
Condition 配合 ReentrantLock 使用,await() 和 signalAll() 分别对应等待与通知。与对象监视器相比,一把锁可以创建多个条件队列,例如队列的“非空”和“未满”各用一个条件。
用法仍然是:加锁,循环判断条件,等待;修改条件后发出通知,最后释放锁。await() 会释放关联锁,返回前再获取它。一把锁对应多个条件时,可以分别通知等待数据和等待空位的线程。
LockSupport
LockSupport 直接指定要唤醒的线程,不需要持有对象监视器:
import java.util.concurrent.locks.LockSupport;
public class ParkDemo {
private static volatile boolean ready;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!ready) {
if (Thread.currentThread().isInterrupted()) {
return;
}
LockSupport.park();
}
System.out.println("继续执行");
});
worker.start();
ready = true;
LockSupport.unpark(worker);
worker.join();
}
}可以把 unpark() 理解为给线程一个许可,park() 消耗许可。如果已经有许可,park() 就直接返回;许可最多只有一个,连续调用两次 unpark() 不会攒出两个。
它允许在目标线程启动后先 unpark()、再 park(),但依然要用条件循环。park() 也可能因为中断或虚假唤醒返回。本例用 volatile 保证条件的可见性,并单独处理了中断退出。
另外,park() 不会帮你释放已经持有的锁。如果拿着一把锁等待,而负责通知的线程又需要这把锁,照样可能把程序卡住。