Android Condition 笔记

1. 什么是 Condition?

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

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


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

特性 Object.wait/notify Condition
锁的绑定 必须与 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. 经典使用范式:生产者-消费者(多条件)

java 复制代码
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 轻松实现:

kotlin 复制代码
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 依然能帮助你深入理解并发编程的本质,也让你在维护底层代码或分析开源框架时游刃有余。

相关推荐
肖。35487870941 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
城东米粉儿2 小时前
Android 动态加载 Activity
android
城东米粉儿2 小时前
Android lancet 笔记
android
zh_xuan3 小时前
React Native 原生和RN互相调用以及事件监听
android·javascript·react native
哈哈浩丶4 小时前
LK(little kernel)-3:LK的启动流程-作为Android的bootloarder
android·linux·服务器
Android系统攻城狮12 小时前
Android tinyalsa深度解析之pcm_get_delay调用流程与实战(一百一十九)
android·pcm·tinyalsa·音频进阶·android hal·audio hal
·云扬·14 小时前
MySQL基于位点的主从复制完整部署指南
android·mysql·adb
千里马-horse14 小时前
Building a Simple Engine -- Mobile Development -- Platform considerations
android·ios·rendering·vulkan
吴声子夜歌15 小时前
RxJava——Subscriber
android·echarts·rxjava