Java 实例 – 线程挂起:从概念到实战
在多线程编程的世界里,线程就像一群正在奔跑的快递员。每个快递员(线程)都负责一个任务,比如送货、打包、登记。但有时候,某个快递员在送货途中发现目的地暂时无法送达,比如客户不在家、仓库没开门。这时候,他不能一直傻等,也不能强行闯入,而是选择“暂停工作”,等待条件满足后再继续。这种“暂停”的行为,就是我们常说的“线程挂起”。
在 Java 中,线程挂起并不是一个直接的 API 操作,而是通过一系列机制来实现的。理解这些机制,是掌握并发编程的关键一步。本文将通过真实代码示例,带你一步步搞懂 Java 实例 – 线程挂起的原理与用法。
什么是线程挂起?
线程挂起(Thread Suspension)是指让一个正在运行的线程暂时停止执行,进入等待状态,直到某个条件满足后再恢复运行。这在多任务处理中非常常见,比如:
- 等待某个资源释放
- 等待用户输入
- 等待数据库查询结果返回
虽然 Java 早期版本曾提供 suspend() 和 resume() 方法,但它们因容易导致死锁,已被标记为 过时(deprecated)。现在的 Java 通过 wait()、notify()、notifyAll() 以及 Lock 和 Condition 等机制来安全地实现线程挂起。
💡 小贴士:线程挂起 ≠ 线程终止。挂起是暂停,终止是彻底结束。就像你暂时关掉手机,但手机还在,只是不响了。
使用 wait() 和 notify() 实现线程挂起
wait() 和 notify() 是最经典、最基础的线程挂起机制,它们必须配合 synchronized 使用,属于对象级别的操作。
原理说明
wait():让当前线程释放锁并进入等待状态,直到其他线程调用notify()或notifyAll()。notify():唤醒一个正在等待该对象锁的线程。notifyAll():唤醒所有等待该对象锁的线程。
这就像一个“排队等餐”的餐厅:你坐在位置上,服务员告诉你“稍等一下”,你就放下筷子(释放锁),坐下来等。当菜做好了,服务员喊“上菜了”,你才继续吃饭。
实际代码示例
public class WaitNotifyExample {
private final Object lock = new Object(); // 用于同步的锁对象
private boolean isReady = false; // 标记数据是否准备就绪
// 生产者线程:负责准备数据
public void producer() {
synchronized (lock) {
System.out.println("生产者开始准备数据...");
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("生产者被中断");
return;
}
isReady = true; // 数据准备完成
System.out.println("生产者:数据已准备就绪,通知等待线程");
lock.notify(); // 唤醒一个等待的线程
}
}
// 消费者线程:等待数据并消费
public void consumer() {
synchronized (lock) {
// 如果数据未准备就绪,线程将进入等待状态
while (!isReady) {
try {
System.out.println("消费者:数据尚未就绪,进入等待...");
lock.wait(); // 释放锁并等待,直到被唤醒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("消费者被中断");
return;
}
}
System.out.println("消费者:数据已就绪,开始消费");
// 消费数据的逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
// 启动生产者线程
Thread producerThread = new Thread(example::producer);
// 启动消费者线程
Thread consumerThread = new Thread(example::consumer);
// 先启动消费者,让它先等待
consumerThread.start();
try {
Thread.sleep(1000); // 稍等一下,确保消费者先启动
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 再启动生产者,触发通知
producerThread.start();
try {
producerThread.join(); // 等待生产者结束
consumerThread.join(); // 等待消费者结束
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
📌 代码说明:
synchronized(lock)确保同一时间只有一个线程能访问共享资源。while (!isReady)而不是if,是为了防止“虚假唤醒”(spurious wakeup)——即使没有被notify(),线程也可能醒来,所以必须重新检查条件。lock.wait()会自动释放锁,并让线程进入等待队列。lock.notify()唤醒一个等待线程,但不会立即恢复执行,需重新获取锁。
使用 Lock 和 Condition 实现更精细的挂起控制
wait() 和 notify() 虽然经典,但使用起来不够灵活,比如不能指定唤醒哪个线程。Java 5 引入了 java.util.concurrent.locks 包,提供了更强大的锁机制。
Lock 与 Condition 的优势
- 可以创建多个
Condition,实现“按条件唤醒”。 - 支持公平锁、可中断锁、超时锁等高级特性。
- 更清晰的代码结构,避免死锁风险。
实际代码示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockConditionExample {
private final Lock lock = new ReentrantLock(); // 可重入锁
private final Condition condition = lock.newCondition(); // 创建条件对象
private boolean isReady = false;
// 生产者方法
public void producer() {
lock.lock(); // 获取锁
try {
System.out.println("生产者:开始准备数据...");
Thread.sleep(2000);
isReady = true;
System.out.println("生产者:数据准备完成,通知等待线程");
condition.signal(); // 唤醒一个等待的线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("生产者被中断");
} finally {
lock.unlock(); // 一定要释放锁
}
}
// 消费者方法
public void consumer() {
lock.lock(); // 获取锁
try {
while (!isReady) {
System.out.println("消费者:数据未就绪,等待中...");
condition.await(); // 等待,释放锁,进入等待状态
}
System.out.println("消费者:数据已就绪,开始消费");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("消费者被中断");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockConditionExample example = new LockConditionExample();
Thread producer = new Thread(example::producer);
Thread consumer = new Thread(example::consumer);
consumer.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
producer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
📌 关键点:
lock.lock()和lock.unlock()必须成对出现,通常放在try-finally中,防止锁未释放。condition.await()相当于wait(),但更灵活。condition.signal()相当于notify()。- 可以创建多个
Condition实现多条件等待,比如“库存不足”、“订单超时”等。
线程挂起的常见误区与最佳实践
在实际开发中,很多线程挂起的问题源于对机制的理解不深。以下是几个常见误区:
| 误区 | 正确做法 |
|---|---|
用 Thread.suspend() 和 resume() |
已废弃,极易造成死锁,应避免使用 |
在 if 中使用 wait() |
应使用 while 循环判断条件,防止虚假唤醒 |
忘记在 finally 中释放锁 |
使用 try-finally 确保锁一定释放 |
多个线程竞争同一条件但未使用 notifyAll() |
用 notifyAll() 更安全,避免唤醒遗漏 |
✅ 最佳实践建议:
- 优先使用
Lock和Condition,比synchronized更灵活。- 所有等待逻辑都用
while包裹,而非if。- 线程挂起期间不要持有大锁,避免阻塞其他线程。
为什么线程挂起是并发编程的核心?
想象一个电商系统:用户下单时,系统需要检查库存。如果库存不足,就不能立即扣减。这时,系统可以挂起当前线程,等待库存补货。等到补货完成,再唤醒线程继续处理订单。这正是线程挂起的典型应用场景。
没有线程挂起机制,系统只能不断轮询(busy-wait),浪费 CPU 资源。而通过挂起,系统能高效地利用资源,实现高并发、低延迟。
Java 实例 – 线程挂起,正是构建高性能并发程序的基石之一。掌握它,你就迈出了成为高级 Java 开发者的坚实一步。
总结
本文从线程挂起的概念出发,通过 wait()/notify() 和 Lock/Condition 两种主流方式,详细演示了如何实现线程安全的挂起与唤醒。我们不仅讲解了原理,还提供了可运行的代码示例,并指出了常见陷阱。
无论你是初学者还是中级开发者,理解并掌握线程挂起机制,都对提升你的并发编程能力至关重要。记住:线程挂起不是“暂停”,而是“智慧地等待”。
在未来的并发编程中,你会频繁遇到“等待资源”“等待条件满足”的场景。而 Java 实例 – 线程挂起,正是解决这些问题的钥匙。