Java基础教程(143)线程同步之使用Condition:告别synchronized和wait/notify!深度揭秘Java线程同步新纪元——Condition接口
在多线程编程中,精准控制线程协作是核心挑战。Java内置的synchronized配合wait()/notify()机制粗粒度高,易导致死锁与性能瓶颈。本文深度剖析java.util.concurrent.locks.Condition接口,揭示其如何通过“条件队列”实现线程间精细化管理与高效通信。通过一个经典的生产者-消费者示例,您将直观体会Condition如何分离等待集,实现定向通知,从而提
深度分析Java线程同步之使用Condition
在多线程的世界里,协调不同线程之间的工作是确保数据一致性和程序正确性的基石。传统的synchronized关键字配合Object的wait()、notify()和notifyAll()方法虽然基础,但其“一个锁对应一个等待队列”的模型显得过于粗放,无法满足更精细化的线程调度需求。
一、为何需要Condition?
synchronized的监视器模型存在一个明显缺陷:当持有锁的线程需要等待某个特定条件(如“缓冲区非满”或“缓冲区非空”)时,它只能进入一个统一的等待集合。当条件满足时,我们无法精确地唤醒那些只等待特定条件的线程(例如只唤醒生产者或只唤醒消费者),只能通过notifyAll()唤醒所有等待线程,让它们去竞争锁并重新检查条件。这会导致大量的上下文切换和锁竞争,性能低下且容易产生“惊群效应”。
Condition接口的出现,正是为了解决这一问题。它将一个锁(Lock)关联了多个等待条件,允许线程为不同的条件在不同的集合中等待,从而实现更精确、更高效的线程间通信。
二、Condition核心机制
Condition对象是由Lock对象调用newCondition()方法创建的。你可以将其理解为一个绑定在特定锁上的“条件队列”。
其核心方法与传统方法对应,但更清晰、安全:
await()->wait(): 释放当前占有的锁,并在此条件上等待。signal()->notify(): 唤醒在此条件上等待的一个线程。signalAll()->notifyAll(): 唤醒在此条件上等待的所有线程。
关键优势:
- 精细通知:一个锁可以创建多个
Condition实例。例如,可以为“非空”和“非满”各创建一个条件队列。当缓冲区为空时,消费者在“非空”条件上等待;当生产者放入数据后,只需signal“非空”条件队列上的一个消费者,而不会错误地唤醒其他生产者线程。 - 可中断与超时:
Condition提供了更丰富的等待方法,如awaitUninterruptibly()(不可中断等待)、awaitNanos(long nanosTimeout)(超时等待),灵活性远超传统方式。 - 公平性:与
ReentrantLock的公平锁策略配合使用时,Condition的等待线程唤醒顺序可以遵循公平原则。
三、实战示例:生产者-消费者模型
下面是一个使用ReentrantLock和两个Condition实现的有界缓冲区生产者-消费者模型。它高效地解决了“非空”和“非满”两个条件的等待与通知问题。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBufferWithCondition {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); // “非满”条件
final Condition notEmpty = lock.newCondition(); // “非空”条件
final Object[] items = new Object[10]; // 容量为10的缓冲区
int putptr, takeptr, count; // 放入索引、取出索引、当前数量
// 生产者方法:放入数据
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) { // 1. 如果缓冲区满了
notFull.await(); // 就在“非满”条件上等待
}
items[putptr] = x;
if (++putptr == items.length) putptr = 0; // 环形队列
++count;
notEmpty.signal(); // 2. 放入成功后,通知消费者“非空”了
} finally {
lock.unlock();
}
}
// 消费者方法:取出数据
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) { // 1. 如果缓冲区空了
notEmpty.await(); // 就在“非空”条件上等待
}
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0; // 环形队列
--count;
notFull.signal(); // 2. 取出成功后,通知生产者“非满”了
return x;
} finally {
lock.unlock();
}
}
}
四、深度分析
while循环检查条件:无论是await()前还是被唤醒后,都必须用while循环重新检查条件。因为被唤醒时,条件可能再次变得不满足(例如,另一个消费者抢先取走了数据)。- 信号的选择:示例中使用了
signal()而非signalAll()。这是一种优化,因为它只唤醒一个线程,减少了竞争。在大多数情况下,这已经足够,因为每次put或take操作只会改变一个单位的状态,只需要唤醒一个对应的线程。如果一次操作可以满足多个等待线程的条件(例如,putN操作放入多个数据),则可以考虑使用signalAll()。 - 锁的释放与重新获取:调用
await()会原子地释放锁并进入等待。当被signal()唤醒后,线程在从await()返回前会重新获取锁,保证了线程安全。
结论
Condition接口将传统的对象监视器方法(wait, notify, notifyAll)分解为 distinct objects(不同的条件对象),通过将锁与多个等待集组合使用,提供了比synchronized更精细、更灵活、性能更高的线程协作手段。对于构建复杂的并发组件(如ArrayBlockingQueue等JUC集合),它是不可或缺的核心工具。掌握Condition,意味着你的并发编程能力从“能用”迈向了“精通”的新高度。
更多推荐



所有评论(0)