Java并发编程:使用Condition类实现精确线程通信
Condition是JDK 5引入的高级线程通信工具,它允许将多个等待集绑定到同一个锁上,从而解决传统synchronized关键字无法区分通知对象的缺陷。本文将深入解析Condition的工作机制,并提供基于ReentrantLock的实战代码。
Condition与传统监视器的区别
在Java中,Object类的wait()、notify()和notifyAll()方法用于同步对象,但它们必须与"同步锁"(即synchronized关键字)捆绑使用,且每个对象只有一个等待队列。这导致程序难以针对特定条件的线程进行唤醒,容易引发虚假唤醒或唤醒错误的线程。
Condition类打破了这种限制。Condition需要与"互斥锁"(如java.util.concurrent.locks.Lock)组合使用。其核心优势在于:
- 解耦关联:一个Lock可以创建多个Condition实例,每个Condition对应特定的业务逻辑或线程状态。
- 精确唤醒:通过signal()唤醒指定条件队列中的一个线程,或者通过signalAll()唤醒全部,避免了全局干扰。
核心方法对照表
理解Condition的功能,最直接的方式是与Object方法进行对比:
// 造成当前线程在接到信号或被中断之前一直处于等待状态。
void await()
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态。(不可中断)
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程。
void signal()
// 唤醒所有等待线程。
void signalAll()
基础用法示例
以下示例展示了如何使用Condition替代synchronized进行简单的线程交替执行。相比传统的notify/notifyAll,这里的逻辑更清晰,lock/unlock结构也更加规范。
public class ThreadTest2 {
public static void main(String[] args) {
final Business business = new Business();
// 启动子线程
new Thread(new Runnable() {
@Override
public void run() {
threadExecute(business, "sub");
}
}).start();
// 主线程执行
threadExecute(business, "main");
}
public static void threadExecute(Business business, String threadType) {
for(int i = 0; i < 100; i++) {
try {
if("main".equals(threadType)) {
business.main(i);
} else {
business.sub(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Business {
private boolean bool = true;
private Lock lock = new ReentrantLock();
// 为Lock创建Condition对象
private Condition condition = lock.newCondition();
public /*synchronized*/ void main(int loop) throws InterruptedException {
lock.lock();
try {
// 使用while防止虚假唤醒
while(bool) {
condition.await();//相当于this.wait()
}
for(int i = 0; i < 100; i++) {
System.out.println("main thread seq of " + i + ", loop of " + loop);
}
bool = true;
condition.signal();//相当于this.notify(), 唤醒sub线程
} finally {
lock.unlock();
}
}
public /*synchronized*/ void sub(int loop) throws InterruptedException {
lock.lock();
try {
while(!bool) {
condition.await();
}
for(int i = 0; i < 10; i++) {
System.out.println("sub thread seq of " + i + ", loop of " + loop);
}
bool = false;
condition.signal();//相当于this.notify(), 唤醒main线程
} finally {
lock.unlock();
}
}
}
进阶案例:基于多Condition的有界缓存区
上述例子虽然能用,但本质上还是单Condition。Condition真正强大的地方体现在生产者-消费者模式中。当存在多个阻塞条件时(例如:缓冲区满需等待写操作完成,缓冲区空需等待读操作完成),如果使用单个Condition,可能导致唤醒错误的线程,产生性能浪费甚至死循环。
下面是一个标准的有界缓存区实现:
class BoundedBuffer {
final Lock lock = new ReentrantLock();//锁对象
final Condition notFull = lock.newCondition();//写线程条件:缓冲区不满时可写
final Condition notEmpty = lock.newCondition();//读线程条件:缓冲区非空时可读
final Object[] items = new Object[100];//缓存队列
int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
// 如果队列满了,则挂起当前写线程
while (count == items.length)
notFull.await();
items[putptr] = x;//赋值
// 环形数组索引处理
if (++putptr == items.length) putptr = 0;
++count;//计数加一
// 写入成功后,唤醒一个正在等待数据的读线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// 如果队列为空,则挂起当前读线程
while (count == 0)
notEmpty.await();
Object x = items[takeptr];//取值
// 环形数组索引处理
if (++takeptr == items.length) takeptr = 0;
--count;//计数减一
// 读取后腾出空间,唤醒一个正在等待空间的写线程
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
执行流程解析
- 写线程进入put方法:首先检查
count == items.length,若已满,调用notFull.await()挂起自己,并释放锁。 - 读线程有机会进入take方法:发现数据可用,读取数据并将
count--,然后调用notFull.signal()唤醒一个挂起的写线程。 - 写线程被唤醒:重新获得锁,跳出循环继续写入。同时,写线程结束后调用
notEmpty.signal()唤醒读线程。
这种设计保证了针对性唤醒:只有在需要时才唤醒对应的线程集合。相比之下,若仅使用单一的this.notify(),当缓存区满时唤醒的是任意线程(可能是另一个写线程),该线程醒来后发现依然满而再次挂起,造成无谓的上下文切换开销。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













