Java 线程中的 wait 机制:深入理解与实践
简介
在 Java 多线程编程中,wait
机制是一个非常重要的工具,它允许线程之间进行有效的通信和协调。通过 wait
,线程可以暂停执行,直到被其他线程唤醒。这在很多场景下都非常有用,比如生产者 - 消费者模型,线程需要等待某些条件满足后再继续执行。本文将深入探讨 wait
在 Java 线程中的基础概念、使用方法、常见实践以及最佳实践。
目录
- 基础概念
wait
方法的定义- 线程状态与
wait
- 使用方法
wait
方法的调用方式notify
与notifyAll
方法
- 常见实践
- 生产者 - 消费者模型
- 线程同步与资源共享
- 最佳实践
- 避免死锁
- 合理使用
wait
和notify
- 小结
- 参考资料
基础概念
wait
方法的定义
wait
方法是 java.lang.Object
类的成员方法,这意味着 Java 中的任何对象都可以调用 wait
方法。它有三种重载形式:
- void wait()
:导致当前线程等待,直到其他线程调用该对象的 notify
或 notifyAll
方法。
- void wait(long timeout)
:导致当前线程等待,直到其他线程调用该对象的 notify
或 notifyAll
方法,或者指定的毫秒数过去。
- void wait(long timeout, int nanos)
:与上一个方法类似,但可以指定更精确的等待时间,包括纳秒。
线程状态与 wait
当一个线程调用对象的 wait
方法时,它会释放对象的锁,并进入等待状态。在等待状态下,线程不会占用 CPU 资源,直到被其他线程唤醒。当其他线程调用该对象的 notify
或 notifyAll
方法时,等待的线程会被唤醒,重新竞争对象的锁,获取锁后继续执行。
使用方法
wait
方法的调用方式
wait
方法必须在同步代码块或同步方法中调用,否则会抛出 IllegalMonitorStateException
异常。下面是一个简单的示例:
public class WaitExample {
public static void main(String[] args) {
Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程开始等待...");
lock.wait();
System.out.println("线程被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println("主线程唤醒等待线程...");
lock.notify();
}
}
}
在这个示例中,我们创建了一个线程,该线程在获取 lock
对象的锁后调用 wait
方法,进入等待状态。主线程休眠 2 秒后,获取 lock
对象的锁,并调用 notify
方法唤醒等待的线程。
notify
与 notifyAll
方法
notify
方法用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,那么只会唤醒其中一个线程,具体唤醒哪个线程是由 JVM 决定的。
notifyAll
方法用于唤醒在此对象监视器上等待的所有线程。所有被唤醒的线程会竞争对象的锁,获取锁的线程将继续执行。
public class NotifyExample {
public static void main(String[] args) {
Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程 1 开始等待...");
lock.wait();
System.out.println("线程 1 被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程 2 开始等待...");
lock.wait();
System.out.println("线程 2 被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println("主线程唤醒所有等待线程...");
lock.notifyAll();
}
}
}
在这个示例中,我们创建了两个线程,它们都在等待 lock
对象的唤醒。主线程休眠 2 秒后,调用 notifyAll
方法唤醒所有等待的线程。
常见实践
生产者 - 消费者模型
生产者 - 消费者模型是 wait
和 notify
机制的经典应用场景。在这个模型中,生产者线程生成数据并将其放入共享缓冲区,消费者线程从共享缓冲区中取出数据进行处理。当缓冲区满时,生产者线程需要等待;当缓冲区空时,消费者线程需要等待。
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
private static final int MAX_SIZE = 5;
private static Queue<Integer> queue = new LinkedList<>();
public static class Producer implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (queue) {
while (queue.size() == MAX_SIZE) {
try {
System.out.println("缓冲区已满,生产者等待...");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(i);
System.out.println("生产者生产: " + i);
queue.notify();
}
}
}
}
public static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("缓冲区为空,消费者等待...");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int item = queue.poll();
System.out.println("消费者消费: " + item);
queue.notify();
}
}
}
}
public static void main(String[] args) {
Thread producerThread = new Thread(new Producer());
Thread consumerThread = new Thread(new Consumer());
producerThread.start();
consumerThread.start();
}
}
在这个示例中,Producer
线程不断生成数据并放入 queue
中,当 queue
满时,调用 wait
方法等待;Consumer
线程不断从 queue
中取出数据,当 queue
为空时,调用 wait
方法等待。通过 notify
方法,生产者和消费者线程可以相互唤醒。
线程同步与资源共享
在多线程环境中,多个线程可能需要访问和修改共享资源。为了避免数据竞争和不一致性,我们可以使用 wait
和 notify
机制来实现线程同步。
public class ResourceSharingExample {
private static int sharedResource = 0;
public static class Incrementer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (ResourceSharingExample.class) {
sharedResource++;
System.out.println("Incrementer: " + sharedResource);
ResourceSharingExample.class.notify();
}
}
}
}
public static class Decrementer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (ResourceSharingExample.class) {
while (sharedResource == 0) {
try {
System.out.println("资源为 0,Decrementer 等待...");
ResourceSharingExample.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sharedResource--;
System.out.println("Decrementer: " + sharedResource);
ResourceSharingExample.class.notify();
}
}
}
}
public static void main(String[] args) {
Thread incrementerThread = new Thread(new Incrementer());
Thread decrementerThread = new Thread(new Decrementer());
incrementerThread.start();
decrementerThread.start();
}
}
在这个示例中,Incrementer
线程和 Decrementer
线程共享 sharedResource
变量。Decrementer
线程在 sharedResource
为 0 时调用 wait
方法等待,Incrementer
线程增加 sharedResource
后调用 notify
方法唤醒 Decrementer
线程。
最佳实践
避免死锁
死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁。为了避免死锁,我们可以采取以下措施:
- 尽量减少锁的使用范围,只在必要的代码块中使用同步。
- 按照相同的顺序获取锁,避免不同线程以不同顺序获取锁。
- 使用 tryLock
方法来尝试获取锁,并设置合理的超时时间。
合理使用 wait
和 notify
- 在调用
wait
方法前,确保已经获取了对象的锁。 - 在
wait
方法的条件判断中使用while
循环,而不是if
语句,以防止虚假唤醒。 - 尽量使用
notifyAll
方法,除非你确定只需要唤醒一个线程。这样可以避免某些线程永远无法被唤醒的情况。
小结
wait
机制是 Java 多线程编程中实现线程通信和同步的重要工具。通过合理使用 wait
、notify
和 notifyAll
方法,我们可以有效地协调多个线程的执行,解决诸如生产者 - 消费者模型、资源共享等实际问题。同时,遵循最佳实践可以帮助我们避免死锁等常见问题,提高多线程程序的稳定性和可靠性。
参考资料
- Java 官方文档 - Object 类
- 《Effective Java》 - Joshua Bloch
- 《Java Concurrency in Practice》 - Brian Goetz
希望通过本文的介绍,读者能够深入理解并高效使用 wait
在 Java 线程中的相关知识。如果有任何问题或建议,欢迎在评论区留言。