Android Condition 笔记

0 阅读7分钟

1. 什么是 Condition?

Conditionjava.util.concurrent.locks 包下的一个接口,它和 Lock 配合使用,用来实现线程间的等待/通知(wait/notify)机制。可以把 Condition 理解为传统 Object 监视器方法(waitnotifynotifyAll)的现代化升级版,它提供了更精细的控制和更丰富的功能。

简单来说,当线程获取了锁之后,如果某个条件不满足(比如队列为空),就可以调用 Condition.await() 让自己进入等待状态,并释放锁;当另一个线程改变了条件(比如往队列里放了数据),就可以调用 Condition.signal()signalAll() 唤醒等待的线程。


2. 为什么要用 Condition?和 wait/notify 比强在哪?

特性Object.wait/notifyCondition
锁的绑定必须与 synchronized 配合使用,一个对象只有一个隐式的条件队列。可以与 Lock 配合,一个 Lock 上可以创建多个 Condition 实例,实现多路等待/通知。
多条件支持一个对象只有一个等待队列,无法区分不同的等待原因(比如“队列已满”和“队列为空”混在一起)。可以创建多个 Condition,例如 notFullnotEmpty,分别管理不同条件的等待线程,避免不必要的唤醒。
中断响应wait() 会抛出 InterruptedException,但不能在等待期间响应中断?实际上 wait 是可以响应中断的。但 Condition 提供了更灵活的中断策略:awaitUninterruptibly() 可以在等待时不响应中断。await() 默认响应中断,同时还有 awaitUninterruptibly() 方法。
超时等待支持 wait(long timeout)支持更丰富的超时:await(long time, TimeUnit unit)awaitNanos(long nanosTimeout)awaitUntil(Date deadline),可以精确控制超时行为。
公平性等待线程的唤醒顺序不可控(依赖于JVM实现)。如果 Lock 是公平锁,那么 Condition 的等待队列也是公平的,可以按 FIFO 顺序唤醒。
线程转储通过 synchronized 等待的线程在转储中容易识别。通过 Condition 等待的线程同样会在转储中标记为 WAITING (parking),配合 Lock 信息,也很容易诊断。

在复杂的并发组件(比如阻塞队列、线程池)中,Condition 几乎是标配。比如 ArrayBlockingQueue 内部就用了两个 ConditionnotEmptynotFull,分别管理“取数据时队列空”和“存数据时队列满”的等待线程。


3. Condition 的核心方法

要使用 Condition,必须先通过 Lock.newCondition() 创建实例。注意:必须在持有对应 Lock 的前提下调用这些方法,否则会抛出 IllegalMonitorStateException

  • void await() throws InterruptedException
    使当前线程进入等待状态,直到被 signal 或中断。调用时会释放锁,被唤醒后会重新尝试获取锁,获取成功后才会从 await 返回。

  • void awaitUninterruptibly()
    等待过程中不响应中断,即使线程被中断也会继续等待,直到被 signal。返回后可以通过 Thread.interrupted() 检查中断状态。

  • long awaitNanos(long nanosTimeout) throws InterruptedException
    等待指定的纳秒数,返回值是剩余时间(如果超时则返回 0 或负数)。可以用这个实现超时控制。

  • boolean await(long time, TimeUnit unit) throws InterruptedException
    等待指定时间,超时返回 false,被唤醒返回 true

  • boolean awaitUntil(Date deadline) throws InterruptedException
    等待直到某个绝对时间点,超时返回 false,被唤醒返回 true

  • void signal()
    唤醒一个在此 Condition 上等待的线程(如果有多个,选择策略取决于实现,通常是队列头部)。

  • void signalAll()
    唤醒所有在此 Condition 上等待的线程。


4. 经典使用范式:生产者-消费者(多条件)

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition(); // 队列非空条件
    private final Condition notFull = lock.newCondition();  // 队列未满条件

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 队列已满,等待“非满”条件
            }
            queue.add(item);
            notEmpty.signal(); // 唤醒等待“非空”的消费者
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 队列为空,等待“非空”条件
            }
            T item = queue.poll();
            notFull.signal(); // 唤醒等待“非满”的生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

关键点

  • 使用 while 循环检查条件(而不是 if),防止虚假唤醒(spurious wakeup)。这是 Java 并发编程的铁律。
  • puttake 分别操作不同的 Condition,避免了像 notifyAll 那样唤醒所有线程造成的竞争。
  • 务必在 finally 中释放锁。

5. 在 Android 开发中的实践与注意事项

5.1 适用场景

在 Android 中,主线程(UI线程)绝对不能阻塞,所以 Condition 主要用于后台线程之间的协调。常见的场景有:

  • 自定义阻塞队列:用于生产者消费者模式,比如处理下载任务的队列、日志写入队列等。
  • 线程池的管理:比如自定义线程池,需要根据任务数量控制线程的挂起与唤醒。
  • 异步任务的分批处理:比如等待某个条件满足后,再批量执行任务。

5.2 注意事项

  • 务必在持有锁时调用 await/signal,否则抛出异常。
  • await 后一定要用 while 循环检查条件,这是避免虚假唤醒和复杂竞争的必要手段。
  • signalsignalAll 的选择signal 可能更高效,但如果你不确定唤醒哪个线程,或者等待线程可能因为不同原因等待,使用 signalAll 更安全(但可能会造成“惊群效应”)。在多数业务场景中,用 signal 配合多条件已经足够。
  • 避免在 await 时持有其他锁,否则可能导致死锁。
  • 性能考虑Condition 基于 LockSupportpark 实现,比 wait/notify 更轻量?实际上二者底层实现不同,但性能差异不大。不过 Condition 的灵活性和可控性带来的收益远大于微小的性能差异。

5.3 与 Kotlin 协程的关系

如果你现在用 Kotlin 开发新项目,很可能不再直接操作线程和 Condition,而是使用协程。Kotlin 协程提供了 Mutex 配合 withLock,以及 ChannelFlow 等高级原语。例如,生产者-消费者可以用 Channel 轻松实现:

val channel = Channel<Int>(capacity = 10)
// 生产者
launch { channel.send(42) }
// 消费者
launch { println(channel.receive()) }

Channel 内部也使用了类似 Condition 的机制(在 JVM 上基于 LockSupport 实现),但对开发者完全透明。所以,如果是纯 Kotlin 项目,建议优先使用协程。

但是,如果你在维护老项目,或者需要与 Java 线程池交互,或者编写底层库,Condition 依然是不可或缺的工具。


6. 常见陷阱与调试

  • 信号丢失:如果在调用 signal 时没有线程在等待,信号就会丢失。这通常是正确的行为,但有时会被误解。比如,在 put 时先 signal 后释放锁,但如果消费者还没开始等待,那么这次 signal 就浪费了,但没关系,因为数据已经存在,消费者下次 take 时不会等待。所以,信号是否丢失取决于业务逻辑的设计,需要保证在可能等待之前先持有锁并检查条件。
  • 死锁:如果 await 后忘记在另一条路径上 signal,就会导致线程永久等待。务必检查所有可能改变条件的地方是否都调用了对应的 signal
  • 线程转储分析:当线程卡住时,通过 jstack 可以看到线程状态为 WAITING (parking),并能看到它在等待哪个 Condition(例如 java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)。结合代码,可以快速定位问题。

总结

Condition 是 Java 并发包中为 Lock 量身定做的线程协调利器。相比传统的 wait/notify,它提供了多条件分离、更灵活的等待/唤醒控制、超时机制以及公平性保证。在 Android 后台任务协调、自定义同步组件开发中非常实用。

不过,随着 Kotlin 协程的普及,很多场景已经被更高级的抽象取代。但是,理解 Condition 依然能帮助你深入理解并发编程的本质,也让你在维护底层代码或分析开源框架时游刃有余。