1.引言
在Java并发编程中,线程间的协作是一个核心话题。为了实现线程间的协作,Java提供了多种机制,其中等待/通知机制是最常见的一种。在早期版本中,我们通过Object类提供的wait
、notify
和notifyAll
方法来实现这种机制。然而,这些方法在使用上存在一些局限性,比如无法支持多个等待条件、唤醒操作不够灵活等。为了克服这些问题,Java在java.util.concurrent.locks
包中引入了Condition
接口。
Condition
接口提供了一组更为灵活和强大的等待/通知方法,它可以与ReentrantLock
等锁配合使用,实现更为复杂的线程同步场景。本文将详细介绍Condition
的使用方法、与Object监视器方法的比较、高级特性以及最佳实践,帮助读者更好地理解和应用这一并发编程利器。
2.Condition的基本使用
在使用Condition
之前,我们需要先获取一个Condition
对象。通常,Condition
对象是通过锁对象获取的。在Java中,ReentrantLock
类提供了newCondition
方法来创建Condition
对象。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
获取到Condition
对象后,我们就可以使用它提供的等待和通知方法了。Condition
接口中定义了以下几个主要方法:
await()
: 使当前线程等待,直到被其他线程唤醒或中断。signal()
: 唤醒在此Condition
对象上等待的一个线程。signalAll()
: 唤醒在此Condition
对象上等待的所有线程。
这些方法的使用方式与Object类的wait
、notify
和notifyAll
方法类似,但提供了更多的灵活性和控制力。
下面,我们通过一个经典的生产者-消费者问题来演示Condition
的基本用法。在这个问题中,生产者和消费者共享一个有限容量的缓冲区,生产者负责生产数据并放入缓冲区,消费者负责从缓冲区取出数据并消费。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items;
int putptr, takeptr, count;
public BoundedBuffer(int capacity) {
this.items = new Object[capacity];
}
// 生产者方法:放入数据
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
// 缓冲区满,等待消费者消费
notFull.await();
}
items[putptr] = item;
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 item = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
// 唤醒等待放数据的生产者
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
在上面的代码中,我们使用了两个Condition
对象:notFull
和notEmpty
,分别表示缓冲区非满和非空的条件。生产者线程在缓冲区满时调用notFull.await()
方法等待,消费者线程在缓冲区空时调用notEmpty.await()
方法等待。当条件满足时,相应的线程会被唤醒,并继续执行。
通过这种方式,我们可以实现生产者和消费者之间的高效协作,避免了忙等和无效唤醒等问题。
3.Condition与Object的监视器方法的比较
在Java中,Object
类提供了wait
、notify
和notifyAll
这三个监视器方法用于线程间的等待和通知。然而,随着并发编程的复杂性增加,这些方法在某些场景下显得捉襟见肘。相比之下,Condition
接口提供了更为丰富和灵活的功能。
- 多条件支持:一个关键的区别在于
Condition
支持多个等待条件。这意味着,对于一个锁,我们可以创建多个Condition
对象,每个对象代表一个不同的等待条件。这在处理复杂的多条件同步问题时非常有用。而Object
的监视器方法则只能支持一个等待条件,即所有线程都在同一个对象上等待和被通知。 - 灵活性:
Condition
提供了可中断等待(awaitInterruptibly
)和定时等待(awaitUntil
)的功能,这使得在等待过程中可以更好地处理中断和超时情况。而Object
的wait
方法则不具备这些特性,一旦线程开始等待,它只能被其他线程显式唤醒或遇到中断异常时才能退出等待状态。 - 与锁的结合:
Condition
是与Lock
接口紧密结合的,它必须配合Lock
使用。这种结合使得Condition
在等待和通知时可以更精细地控制锁的释放和获取。而Object
的监视器方法则是与每个对象自带的内部锁(即synchronized
关键字所使用的锁)结合使用的,这种锁的粒度较大,控制起来相对粗糙。
4.Condition的高级特性
除了基本的使用方法和与Object
监视器方法的比较外,Condition
还提供了一些高级特性,使得它在处理复杂并发问题时更加得心应手。
- 公平与非公平模式:
Condition
的公平与非公平模式取决于与它配合的Lock
的实现。ReentrantLock
类提供了公平和非公平两种模式。在公平模式下,等待时间最长的线程将获得优先执行权;而在非公平模式下,则没有这种保证。这使得Condition
可以根据需要选择不同的同步策略。 - 可中断等待与定时等待:如前所述,
Condition
提供了awaitInterruptibly
和awaitUntil
方法,支持可中断等待和定时等待。这使得在等待过程中可以更好地处理中断和超时情况,提高了程序的响应性和健壮性。
5.常见问题
- 虚假唤醒:
Condition
的await
方法可能会在没有收到通知的情况下返回,这种情况被称为“虚假唤醒”。为了避免这种情况对程序的影响,我们通常在await
方法的调用处使用循环来检查条件是否真正满足。 - 死锁与活锁的预防:在使用
Condition
时,需要注意避免死锁和活锁的发生。死锁是指两个或多个线程无限期地等待彼此释放资源;而活锁则是指线程们不断改变状态以尝试解决问题,但最终无法取得进展。为了避免这些问题,我们可以遵循一些最佳实践,如按顺序获取锁、使用tryLock
方法尝试获取锁等。 - 性能调优建议:在使用
Condition
时,还需要注意性能调优。例如,尽量减少锁的持有时间、避免在持有锁的情况下执行耗时操作等。这些措施可以提高程序的并发性能和响应性。
6.总结
Condition
接口在Java并发编程中的重要性和优势。它提供了更为灵活和强大的等待/通知机制,支持多条件同步、可中断等待和定时等待等高级特性。在使用Condition
时,我们需要注意一些常见问题,以确保程序的正确性和性能。