一个线程需要等另一个线程准备好数据时,可以不断检查标记,也可以先等待,等数据就绪后再被唤醒。后一种方式不用一直占着 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() 不会帮你释放已经持有的锁。如果拿着一把锁等待,而负责通知的线程又需要这把锁,照样可能把程序卡住。

最后修改:2018 年 01 月 13 日
如果觉得我的文章对你有用,请随意赞赏