跳转到正文
Siolin'Log
返回

Java Condition:条件等待、通知与队列转移

概述

Condition 与 Lock 的线程协调关系

Java 的 Condition 是与 Lock 配合使用的线程协调机制。

它解决的不是“多个线程不能同时执行”,而是:

为什么需要 Condition

假设消费者要从队列取数据:

public T take() {
    lock.lock();
    try {
        return queue.remove();
    } finally {
        lock.unlock();
    }
}

虽然锁保证了线程安全,但队列可能为空。

一种错误做法是持有锁不断轮询:

lock.lock();
try {
    while (queue.isEmpty()) {
        // 一直循环
    }

    return queue.remove();
} finally {
    lock.unlock();
}

这段代码存在两个严重问题:

  1. 消费者一直占用 CPU
  2. 消费者始终持有锁,生产者无法获得锁并添加数据

这不属于典型的循环等待死锁,但会造成系统无法推进:消费者一直持锁空转,生产者永久无法进入临界区。

消费者持有锁

消费者等待数据

生产者需要获得锁才能添加数据

生产者永远无法获得锁

Condition.await() 可以让消费者:

  1. 暂时释放锁
  2. 进入等待状态
  3. 让生产者获得锁
  4. 等生产者添加数据后通知消费者
  5. 消费者重新获取锁
  6. 再次检查队列是否非空

核心方法

接口定义可以简化为:

public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout)
            throws InterruptedException;

    boolean await(long time, TimeUnit unit)
            throws InterruptedException;

    boolean awaitUntil(Date deadline)
            throws InterruptedException;

    void signal();

    void signalAll();
}

await():释放锁并等待

基本用法:

lock.lock();

try {
    while (queue.isEmpty()) {
        notEmpty.await();
    }

    return queue.remove();
} finally {
    lock.unlock();
}

假设消费者线程已经获得锁:

flowchart TD
    A["消费者获得 Lock"] --> B["检查 queue.isEmpty()"]
    B --> C{"队列是否为空?"}
    C -- "否:条件满足" --> D["继续消费队列元素"]
    C -- "是:条件不满足" --> E["调用 notEmpty.await()"]
    E --> F["加入 notEmpty 条件队列"]
    F --> G["完全释放 Lock"]
    G --> H["消费者线程挂起"]

生产者获得锁并添加数据:

flowchart TD
    A["生产者获得 Lock"] --> B["向队列添加数据"]
    B --> C["调用 notEmpty.signal()"]
    C --> D["消费者从条件队列<br/>转入锁的同步队列"]
    D --> E["生产者继续持有锁"]
    E --> F["生产者调用 unlock()"]

消费者开始重新竞争锁:

flowchart TD
    A["消费者重新竞争 Lock"] --> B["成功获取 Lock"]
    B --> C["await() 返回"]
    C --> D["重新检查 queue.isEmpty()"]
    D --> E["条件满足,取出数据"]

最关键的一点是:await() 返回之前,当前线程必须重新获得对应的锁。因此,await() 前后,线程都在锁保护的临界区内

await() 是可中断的:

lock.lock();

try {
    while (queue.isEmpty()) {
        notEmpty.await();
    }

    return queue.remove();
} finally {
    lock.unlock();
}

这里让 InterruptedException 直接向上传播即可;只有在当前层吞掉异常、无法继续抛出时,才通常需要重新设置中断状态。

awaitUninterruptibly():不可中断等待

方法定义:

void awaitUninterruptibly();

使用方式:

lock.lock();

try {
    while (!conditionSatisfied()) {
        condition.awaitUninterruptibly();
    }

    doSomething();
} finally {
    lock.unlock();
}

如果线程等待期间收到中断,它不会抛出 InterruptedException,而是继续等待条件成立。

这与 Lock.lock() 的不可中断获取语义类似:接口保证它返回时仍保留等待期间收到的中断状态。具体如何记录和恢复属于实现细节,不应依赖“先清除再重新标记”的内部步骤。

condition.awaitUninterruptibly();

if (Thread.currentThread().isInterrupted()) {
    // 等待期间可能收到过中断
}

signal():通知一个等待线程

基本使用:

lock.lock();

try {
    queue.add(element);
    notEmpty.signal();
} finally {
    lock.unlock();
}

对于 ReentrantLock 返回的 Conditionsignal() 会选择条件队列中的一个等待线程,将其转移到锁的同步等待队列。

signal() 通常会选择条件队列中等待时间较早、尚未取消的线程,但不应把它理解成严格的业务执行顺序。

被选中的线程还需要进入同步队列、重新竞争锁。

Condition 线程从条件队列转移到同步队列

等待队列

区分

这里需要区分 Condition 的等待队列和 Java AQS 的同步队列。

等待获取锁的线程位于 AQS 同步队列:

AQS 同步队列
    [线程 A] → [线程 B] → [线程 C]

调用 await() 的线程位于某个 Condition 的条件队列:

notEmpty 条件队列
    [消费者 1] → [消费者 2]

notFull 条件队列
    [生产者 1] → [生产者 2]

实现

我们可以多次调用 newCondition() 方法创建多个 Condition 对象,也就是一个 lock 可以持有多个等待队列。 一把 Lock 关联多个 Condition 队列

持有多个队列的好处是:提供更细粒度的控制,比如生产者和消费者问题。



上一篇
Java AQS:同步状态、等待队列与阻塞唤醒
下一篇
Java 内存模型:happens-before 与可见性