Java 高级锁机制:Condition 对象与线程等待/通知模式
大家好,今天我们来深入探讨 Java 中高级锁机制的重要组成部分—— Condition
对象,以及它如何与传统的线程等待/通知模式协同工作,实现更精细的线程同步控制。
1. 线程等待/通知模式的背景
在多线程编程中,线程间的协作至关重要。最基本的协作方式是线程间的同步和互斥,保证数据的一致性和避免竞态条件。 Java 提供了 synchronized
关键字和 Object
类的 wait()
, notify()
, notifyAll()
方法来实现线程的等待和通知。
然而,传统的 wait()/notify()
机制存在一些局限性:
- 单一等待队列: 所有调用
wait()
的线程都会进入同一个等待队列,当调用notify()
时,只有一个线程会被唤醒,即使它可能并不满足被唤醒的条件。 - 条件模糊: 线程被唤醒后,需要重新检查条件是否满足,如果仍然不满足,则需要再次调用
wait()
,这导致代码复杂性增加。 - 易出错:
wait()
必须在synchronized
代码块中调用,否则会抛出IllegalMonitorStateException
。 且容易出现虚假唤醒(spurious wakeup),需要使用循环来检查条件。
为了克服这些限制,Java 提供了 Condition
接口,它允许我们将线程放入不同的等待队列中,并根据不同的条件唤醒它们,从而实现更精细的线程同步控制。
2. Condition 接口介绍
Condition
接口是 java.util.concurrent.locks
包的一部分,它通常与 Lock
接口一起使用,提供比 Object
类的 wait()/notify()
更强大的线程等待/通知机制。
Condition
接口的主要方法包括:
方法 | 描述 | 类似于 Object 的方法 |
---|---|---|
await() |
使当前线程进入等待状态,直到被通知 (signal) 或中断。线程会释放与 Condition 关联的锁。 |
wait() |
awaitUninterruptibly() |
与 await() 类似,但线程不会响应中断。 |
N/A |
awaitNanos(long nanosTimeout) |
使当前线程进入等待状态,直到被通知、中断或超时。返回剩余的纳秒数,如果超时,则返回一个非正数。 | wait(long timeout) |
await(long time, TimeUnit unit) |
与 awaitNanos() 类似,但使用 TimeUnit 指定时间单位。返回一个布尔值,表示是否超时。 |
wait(long timeout) |
awaitUntil(Date deadline) |
使当前线程进入等待状态,直到被通知、中断或到达指定时间。返回一个布尔值,表示是否超时。 | N/A |
signal() |
唤醒一个等待在 Condition 上的线程。如果存在多个线程等待,则选择哪个线程唤醒是不确定的。 |
notify() |
signalAll() |
唤醒所有等待在 Condition 上的线程。 |
notifyAll() |
与 Object
类的 wait()/notify()
相比,Condition
接口具有以下优势:
- 多个等待队列: 一个
Lock
可以创建多个Condition
对象,每个Condition
对象都有自己的等待队列,允许线程根据不同的条件进入不同的等待队列。 - 更清晰的语义:
Condition
接口的方法名称更具描述性,例如await()
表示等待,signal()
表示通知。 - 更好的可读性和可维护性: 使用
Condition
可以将等待条件和通知逻辑与锁的获取和释放逻辑分离,从而提高代码的可读性和可维护性。
3. Condition 的使用示例:生产者-消费者模型
经典的生产者-消费者模型是展示 Condition
用法的绝佳例子。 假设我们有一个共享的缓冲区,生产者线程向缓冲区中添加数据,消费者线程从缓冲区中取出数据。为了保证线程安全,我们需要使用锁来保护缓冲区,并使用 Condition
来实现生产者和消费者线程之间的协作。
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 ProducerConsumer {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
System.out.println("Buffer is full, producer waiting...");
notFull.await(); // 缓冲区已满,生产者线程等待
}
queue.offer(item);
System.out.println("Produced: " + item);
notEmpty.signal(); // 通知消费者线程缓冲区已非空
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("Buffer is empty, consumer waiting...");
notEmpty.await(); // 缓冲区为空,消费者线程等待
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notFull.signal(); // 通知生产者线程缓冲区已非满
return item;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ProducerConsumer pc = new ProducerConsumer(5);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.produce(i);
Thread.sleep(100); // Simulate production time
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.consume();
Thread.sleep(200); // Simulate consumption time
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
在这个例子中:
- 我们创建了一个
ReentrantLock
对象lock
,用于保护共享的缓冲区queue
。 - 我们创建了两个
Condition
对象notFull
和notEmpty
,分别用于表示缓冲区是否已满和是否为空。 - 在
produce()
方法中,如果缓冲区已满,生产者线程调用notFull.await()
进入等待状态,释放锁。当消费者线程从缓冲区中取出数据后,调用notFull.signal()
唤醒一个等待在notFull
上的生产者线程。 - 在
consume()
方法中,如果缓冲区为空,消费者线程调用notEmpty.await()
进入等待状态,释放锁。当生产者线程向缓冲区中添加数据后,调用notEmpty.signal()
唤醒一个等待在notEmpty
上的消费者线程。 try-finally
块确保lock.unlock()
始终被调用,即使在await()
或signal()
期间发生异常。
这个例子展示了如何使用 Condition
对象来更精确地控制线程的等待和通知,避免了传统 wait()/notify()
机制的局限性。 使用 Condition
可以让生产者线程只在缓冲区未满时才被唤醒,消费者线程只在缓冲区非空时才被唤醒,提高了线程协作的效率。
4. Condition 的高级用法
除了基本的等待和通知之外,Condition
接口还提供了一些高级用法,例如:
- 超时等待: 可以使用
awaitNanos()
,await(long time, TimeUnit unit)
和awaitUntil()
方法来设置等待超时时间,避免线程永久阻塞。 - 不可中断等待: 可以使用
awaitUninterruptibly()
方法使线程进入不可中断的等待状态,即使线程被中断,它仍然会继续等待。 - 多个等待队列: 一个
Lock
可以创建多个Condition
对象,每个Condition
对象都有自己的等待队列,允许线程根据不同的条件进入不同的等待队列。这可以实现更复杂的线程同步逻辑。
以下是一个使用 await(long time, TimeUnit unit)
实现超时等待的示例:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean signalReceived = false;
public boolean waitForSignal(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
while (!signalReceived) {
if (!condition.await(timeout, unit)) {
// 超时
System.out.println("Timeout occurred!");
return false;
}
}
return true; // 收到信号
} finally {
lock.unlock();
}
}
public void sendSignal() {
lock.lock();
try {
signalReceived = true;
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
TimeoutExample example = new TimeoutExample();
Thread waiter = new Thread(() -> {
try {
boolean received = example.waitForSignal(2, TimeUnit.SECONDS);
if (received) {
System.out.println("Signal received!");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
waiter.start();
Thread.sleep(3000); // 模拟等待一段时间
example.sendSignal(); // 发送信号
waiter.join();
}
}
在这个例子中,waitForSignal()
方法使用 condition.await(timeout, unit)
来等待信号,如果在指定的超时时间内没有收到信号,则返回 false
。这可以避免线程永久阻塞。
5. Condition 与 Object 的 wait/notify 的对比
特性 | Object.wait()/notify() |
Condition.await()/signal() |
---|---|---|
关联对象 | 任何 Java 对象 | 与 Lock 对象关联 |
等待队列 | 单一等待队列 | 多个等待队列(每个 Condition 一个) |
使用范围 | 任何 synchronized 代码块 |
必须在 Lock 的保护下 |
灵活性 | 较低 | 较高 |
错误处理 | 容易出错 | 更清晰 |
可读性 | 较低 | 较高 |
总的来说,Condition
提供了比 Object
的 wait()/notify()
更强大、更灵活、更易于使用的线程等待/通知机制。在需要精细控制线程同步的场景下,Condition
是更好的选择。
6. 使用 Condition 的注意事项
- 必须持有锁:
Condition
的所有方法都必须在与Condition
关联的Lock
对象的保护下调用。 - 虚假唤醒: 与
wait()/notify()
一样,Condition
也可能发生虚假唤醒。因此,在await()
返回后,必须重新检查等待条件是否满足。通常使用while
循环来检查条件。 - 避免死锁: 在使用多个
Condition
对象时,需要 carefully 设计线程的等待和通知逻辑,避免死锁的发生。 - 优先使用高层抽象: 在某些情况下,可以使用 Java 并发包中提供的高层抽象,例如
BlockingQueue
,来简化线程同步代码。
7. Condition 在并发容器中的应用
Condition
接口在 Java 并发容器中也有广泛的应用。 例如,ArrayBlockingQueue
和 LinkedBlockingQueue
等阻塞队列内部就使用了 Condition
对象来实现线程的等待和通知。
这些阻塞队列提供了 put()
和 take()
方法,分别用于向队列中添加元素和从队列中取出元素。 当队列已满时,调用 put()
方法的线程会被阻塞,直到队列中有空闲位置; 当队列为空时,调用 take()
方法的线程会被阻塞,直到队列中有元素。
这些阻塞队列内部使用 Condition
对象来实现这些阻塞和唤醒操作,从而简化了多线程编程。
8. Condition 接口的最佳实践
- 选择合适的锁: 根据具体情况选择合适的
Lock
实现,例如ReentrantLock
或ReentrantReadWriteLock
。 - 创建多个 Condition 对象: 如果需要根据不同的条件进行等待和通知,可以创建多个
Condition
对象,每个Condition
对象对应一个等待队列。 - 使用 while 循环检查等待条件: 在
await()
返回后,必须使用while
循环重新检查等待条件是否满足,以避免虚假唤醒。 - 仔细设计等待和通知逻辑: 在设计复杂的线程同步逻辑时,需要仔细考虑线程的等待和通知顺序,避免死锁的发生。
- 使用高层抽象: 在可能的情况下,优先使用 Java 并发包中提供的高层抽象,例如
BlockingQueue
,来简化线程同步代码。
9. 总结与回顾
今天,我们深入学习了 Java 中高级锁机制的核心—— Condition
对象。我们了解了它的背景、作用、使用方法以及高级应用。Condition
接口通过提供多个等待队列和更清晰的语义,极大地提升了线程同步的灵活性和可控性,弥补了传统 wait()/notify()
机制的不足。掌握 Condition
的使用,是成为一名优秀的并发编程专家的重要一步。希望大家在今后的实践中能够灵活运用 Condition
,写出更高效、更可靠的多线程程序。