在Java中,条件等待(wait)和通知(notify/notifyAll)机制是实现线程间通信的关键手段,它们通常与锁(如synchronized关键字获得的锁)结合使用,以确保线程间的正确同步和数据一致性。这种机制广泛应用于生产者-消费者问题、读写锁、信号量等场景。下面,我将详细介绍如何在Java中实现条件等待和通知机制,并通过一个实际的例子来加深理解。
一、基础概念
1. 锁(Locks)
在Java中,synchronized
关键字或 java.util.concurrent.locks.Lock
接口的实现(如 ReentrantLock
)可用于获取锁。锁的主要作用是控制多个线程对共享资源的访问,防止数据竞争和不一致。
2. 等待(Wait)
当一个线程需要等待某个条件成立时,它会调用某个对象的 wait()
方法。调用 wait()
会使当前线程释放锁并进入等待状态,直到其他线程调用同一个对象的 notify()
或 notifyAll()
方法将其唤醒。
3. 通知(Notify/NotifyAll)
当条件满足时,某个线程会调用 notify()
或 notifyAll()
方法来唤醒等待在该对象上的线程。notify()
方法随机唤醒等待队列中的一个线程,而 notifyAll()
则唤醒所有等待的线程。
二、使用 synchronized 实现条件等待和通知
示例:生产者-消费者问题
在这个例子中,我们将通过 synchronized
关键字和 wait()
/notify()
方法来实现一个简单的生产者-消费者模型。
public class ProducerConsumerExample {
private final Object lock = new Object(); // 锁对象
private int queueSize = 0; // 队列中元素数量
private final int MAX_SIZE = 10; // 队列最大容量
// 生产者方法
public void produce(int value) {
synchronized (lock) {
while (queueSize == MAX_SIZE) {
try {
System.out.println("Queue is full. Producer is waiting.");
lock.wait(); // 等待条件(队列不满)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 生产操作
queueSize++;
System.out.println("Produced: " + value);
// 通知消费者
lock.notify(); // 唤醒一个消费者
}
}
// 消费者方法
public void consume() {
synchronized (lock) {
while (queueSize == 0) {
try {
System.out.println("Queue is empty. Consumer is waiting.");
lock.wait(); // 等待条件(队列不空)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 消费操作
queueSize--;
System.out.println("Consumed");
// 通知生产者
lock.notify(); // 唤醒一个生产者
}
}
public static void main(String[] args) {
ProducerConsumerExample pc = new ProducerConsumerExample();
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
pc.produce(i);
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
for (int i = 0; i < 20; i++) {
pc.consume();
try {
Thread.sleep(200); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
producer.start();
consumer.start();
}
}
三、注意事项
死锁与活锁:在使用
wait()
和notify()
时,必须确保它们的使用不会导致死锁(两个或多个线程无限期地等待对方释放资源)或活锁(线程之间持续交换资源但都无法向前推进)。虚假唤醒:线程可能因为
notify()
或notifyAll()
之外的某些原因被唤醒(虚假唤醒)。因此,在wait()
循环中通常包含条件检查,以确保线程仅在条件真正满足时继续执行。锁的选择:虽然
synchronized
关键字在Java中广泛使用,但在某些情况下,ReentrantLock
等显式锁提供了更灵活的锁定策略,如尝试锁定(tryLock)、定时锁定等。性能考虑:
notifyAll()
可能会唤醒所有等待的线程,即使它们中只有一部分需要继续执行。这可能会导致不必要的线程上下文切换,影响性能。在可能的情况下,考虑使用notify()
来减少唤醒的线程数。
四、进阶使用(码小课推荐)
对于更复杂的并发场景,Java 并发包(java.util.concurrent
)提供了丰富的工具,如 BlockingQueue
、Semaphore
、CountDownLatch
等,这些工具内部已经很好地实现了条件等待和通知机制,使得开发者可以更加专注于业务逻辑的实现。
例如,BlockingQueue
就是一个线程安全的队列,它支持两个附加操作:在元素可用之前阻塞的 take()
方法和在空间可用之前阻塞的 put()
方法。这些操作内部使用了条件变量(Condition Variables)来管理线程的等待和唤醒,而无需开发者直接调用 wait()
和 notify()
。
五、总结
条件等待和通知是Java中处理线程间同步和通信的重要机制。通过 synchronized
关键字或显式锁,结合 wait()
和 notify()
/notifyAll()
方法,可以有效地控制线程的执行顺序和数据访问。然而,开发者在使用这些机制时需要注意避免死锁、活锁和虚假唤醒等问题。此外,对于复杂的并发场景,建议使用Java并发包中提供的更高级别的并发工具,以提高开发效率和系统性能。
在深入理解这些概念的基础上,你可以更加自信地设计并实现高效、可靠的并发程序,提升你作为Java程序员的技能水平。希望这篇文章对你有所帮助,也欢迎你访问码小课网站,获取更多关于Java并发的深入讲解和实战案例。