在Java并发编程中,线程间的通信是一个关键话题。wait()
、notify()
和notifyAll()
是Java中提供的原生方法,用于实现线程间的协调。这些方法在实际开发中非常有用,特别是在生产者-消费者模型中。本文将通过具体案例来讲解这些方法的使用场景和注意事项。
一、线程间通信的基本原理
在多线程环境中,不同线程往往需要协调工作。例如,生产者线程需要将数据放入一个共享缓冲区,而消费者线程则需要从缓冲区中取出数据。当缓冲区满时,生产者必须等待消费者消费数据;当缓冲区为空时,消费者必须等待生产者生成数据。这个等待和通知机制正是通过wait()
、notify()
和notifyAll()
来实现的。
wait()
:使当前线程进入等待状态,直到其他线程调用notify()
或notifyAll()
唤醒它。该方法必须在同步块中调用,并且会释放当前持有的锁。notify()
:唤醒正在等待该对象监视器的单个线程,如果有多个线程在等待,则选择其中一个。该方法也必须在同步块中调用。notifyAll()
:唤醒所有正在等待该对象监视器的线程。
二、生产者-消费者模型示例
为了更好地理解这些方法的实际应用,我们将实现一个简单的生产者-消费者模型。在这个模型中,生产者不断生成数据并放入共享的缓冲区,而消费者则从缓冲区取出数据进行处理。
示例代码:
import java.util.LinkedList;
import java.util.Queue;
class SharedBuffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public SharedBuffer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == capacity) {
System.out.println("Buffer is full, producer waiting...");
wait(); // 等待消费者消费数据
}
queue.offer(value);
System.out.println("Produced: " + value);
notifyAll(); // 通知消费者可以消费数据了
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println("Buffer is empty, consumer waiting...");
wait(); // 等待生产者生产数据
}
int value = queue.poll();
System.out.println("Consumed: " + value);
notifyAll(); // 通知生产者可以生产更多数据了
return value;
}
}
class Producer implements Runnable {
private final SharedBuffer buffer;
public Producer(SharedBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
int value = 0;
try {
while (true) {
buffer.produce(value++);
Thread.sleep(100); // 模拟生产时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private final SharedBuffer buffer;
public Consumer(SharedBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
while (true) {
buffer.consume();
Thread.sleep(150); // 模拟消费时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class Main {
public static void main(String[] args) {
SharedBuffer buffer = new SharedBuffer(5); // 缓冲区容量为5
Thread producerThread = new Thread(new Producer(buffer));
Thread consumerThread = new Thread(new Consumer(buffer));
producerThread.start();
consumerThread.start();
}
}
三、代码解析
在这个示例中,我们创建了一个共享缓冲区SharedBuffer
,并使用两个线程——Producer
和Consumer
——分别执行生产和消费操作。
- SharedBuffer类:
queue
:用作缓冲区的队列。capacity
:定义缓冲区的最大容量。produce()
方法:生产者在缓冲区满时调用wait()
进入等待状态;在成功添加新元素后,调用notifyAll()
唤醒可能等待的消费者线程。consume()
方法:消费者在缓冲区为空时调用wait()
进入等待状态;在成功取出元素后,调用notifyAll()
唤醒可能等待的生产者线程。
- Producer类和Consumer类:
- 这两个类实现了
Runnable
接口,并分别在无限循环中调用produce()
和consume()
方法。 - 为了模拟实际应用中的处理时间,生产者和消费者线程在每次操作后都会休眠一段时间。
- 这两个类实现了
四、使用场景和注意事项
使用场景:
- 生产者-消费者模式:在实际开发中,
wait()
和notify()
非常适合用于实现生产者-消费者模式。典型的应用场景包括日志处理系统、任务调度系统、消息队列等。 - 资源共享管理:在多线程访问共享资源时,使用这些方法可以有效防止资源竞争和不一致的状态。
- 线程池的任务调度:在自定义的线程池实现中,也可以使用
wait()
和notify()
来管理线程的执行和等待。
注意事项:
- 必须在同步块中调用:
wait()
、notify()
和notifyAll()
方法必须在持有锁的情况下调用,否则会抛出IllegalMonitorStateException
。 - 避免死锁:使用
wait()
和notify()
时要特别小心,确保不会引入死锁。通常在编写多线程代码时,需要特别注意锁的顺序和持有时间。 - spurious wakeups:即使没有调用
notify()
或notifyAll()
,等待的线程也可能被唤醒,因此应始终在循环中调用wait()
,并在每次唤醒时重新检查条件。 - 使用
notifyAll()
代替notify()
:在多数情况下,notifyAll()
比notify()
更安全,因为它能唤醒所有等待线程,避免线程永远等待的情况。