Java 多线程:线程中断、等待与唤醒
一、Java 线程中断机制
1. 什么是线程中断?
Java 使用协作式中断机制,通过 interrupt() 通知目标线程需要停止当前任务,但不会强制终止线程。
每个线程都有自己的中断状态,可以简单理解为:
false:没有收到中断请求。true:收到中断请求。
调用 interrupt() 后,线程是否退出,取决于线程自身的处理逻辑。
public class InterruptDemo {
public static void main(String[] args)
throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行业务任务
}
System.out.println("线程退出");
});
thread.start();
Thread.sleep(1000);
// 通知子线程中断
thread.interrupt();
// 等待子线程结束
thread.join();
}
}执行流程:
主线程创建子线程
↓
调用 start()
↓
子线程执行任务
↓
主线程调用 thread.interrupt()
↓
子线程的中断标记设置为 true
↓
子线程通过 isInterrupted() 检测到中断
↓
退出循环
↓
线程进入 TERMINATED 状态重点:interrupt() 只是发出中断请求,并不代表线程一定立即停止。
2. 不同线程状态下的中断行为
interrupt() 的具体效果取决于目标线程当前正在做什么。
特别注意:BLOCKED 不代表所有阻塞情况。
Java 中的 BLOCKED 特指等待获取 synchronized 监视器锁。
例如:
synchronized (LOCK) {
// 等待获取 LOCK
}如果线程正在等待获取 LOCK,其他线程调用 interrupt(),它不会立即结束锁等待,仍需要等待锁释放。
而 ReentrantLock.lockInterruptibly() 支持在等待锁时响应中断。
二、三个中断方法的区别
Java 中最容易混淆的是以下三个方法:
thread.interrupt();
thread.isInterrupted();
Thread.interrupted();1. interrupt():发送中断信号
这是 Thread 的实例方法。
thread.interrupt();作用:请求中断指定线程。
如果目标线程正在正常运行,一般是将其中断标记设置为 true。
2. isInterrupted():检查中断标记
这是 Thread 的实例方法。
thread.isInterrupted();作用:检查指定线程是否被中断。
只检查,不清除中断标记。
例如:
Thread.currentThread().interrupt();
System.out.println(
Thread.currentThread().isInterrupted()
);
System.out.println(
Thread.currentThread().isInterrupted()
);在没有其他操作改变中断状态的情况下,输出:
true
true因为 isInterrupted() 不会清除中断标记。
3. Thread.interrupted():检查并清除中断标记
这是 Thread 的静态方法。
Thread.interrupted();作用:
检查当前线程的中断标记。
返回检查结果。
清除中断标记。
例如:
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted());
System.out.println(
Thread.currentThread().isInterrupted()
);输出:
true
false原因:
第一次调用 Thread.interrupted() 时,中断标记为 true,因此返回 true,并将中断标记清除。
第二次调用 isInterrupted() 时,中断标记已经变为 false。
4. 三个方法总结
另外,以下两个方法虽然都包含 interrupt,但作用不同:
// 请求中断 thread 指向的线程
thread.interrupt();
// 请求中断当前正在执行代码的线程
Thread.currentThread().interrupt();当这段代码在 main 线程中执行时,第二个方法中断的是 main 线程,而不是 thread 指向的子线程。
重点:
interrupt():发送中断请求。isInterrupted():只检查中断状态。Thread.interrupted():检查当前线程并清除状态。Thread.currentThread().interrupt():常用于恢复当前线程的中断状态。
三、InterruptedException 与正确处理方式
1. 为什么 InterruptedException 会清除中断标记?
当线程正在执行 sleep()、wait()、join() 等可中断等待操作时,如果收到中断请求,会通过抛出 InterruptedException 响应中断。
此时中断标记会被清除。
例如:
public class InterruptSleepDemo {
public static void main(String[] args)
throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("收到中断");
System.out.println(
Thread.currentThread().isInterrupted()
);
}
});
thread.start();
Thread.sleep(100);
thread.interrupt();
}
}输出:
收到中断
false原因:
子线程执行
sleep(10000)。主线程调用
thread.interrupt()。子线程的 sleep 被中断。
抛出
InterruptedException,同时清除中断标记。catch 中再次检查中断状态,得到 false。
2. 错误方式:吞掉中断异常
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 什么都不做
}这种写法不推荐。
因为调用方已经发出了中断请求,但当前代码忽略了它,中断状态也被清除,后续逻辑可能无法得知发生过中断。
3. 推荐方式一:向上抛出异常
如果方法允许向上抛出 InterruptedException,优先交给上层处理。
public void executeTask() throws InterruptedException {
Thread.sleep(10000);
}4. 推荐方式二:恢复中断标记
如果不能继续向上抛出 InterruptedException,可以恢复中断标记,让上层逻辑感知中断。
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
// 结束当前任务
return;
}恢复中断标记的作用是:
防止中断信号被吞掉,让后续代码仍然能够感知中断请求。
需要注意:恢复中断标记后,应尽快结束当前任务或交由上层处理,而不是继续执行可能重新进入阻塞状态的无限循环。
5. 正确停止一个线程
public class WorkerDemo {
public static void main(String[] args)
throws InterruptedException {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("处理任务");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
// 退出任务
break;
}
}
System.out.println("线程退出");
});
worker.start();
Thread.sleep(3000);
// 请求中断 worker
worker.interrupt();
// 等待 worker 执行结束
worker.join();
System.out.println("主线程结束");
}
}涉及三个方法:
interrupt():请求停止。isInterrupted():检查中断状态。join():等待目标线程结束。
join() 并不是停止线程,而是让调用者等待目标线程执行完毕。
四、wait()、notify()、notifyAll() 线程通信
1. 三个方法的作用
这三个方法属于 Object 类,主要用于线程之间的等待与唤醒。
注意:这里的锁是指调用对象对应的 synchronized 监视器锁。
2. wait() 的执行原理
synchronized (LOCK) {
LOCK.wait();
}执行过程:
线程获取 LOCK
↓
调用 LOCK.wait()
↓
释放 LOCK 监视器锁
↓
进入 WAITING 状态
↓
等待通知
↓
收到 notify / notifyAll
↓
重新竞争 LOCK
↓
成功获取锁
↓
继续执行注意:
wait() 被唤醒不代表立即执行,必须重新获取监视器锁。
而且 wait() 只会释放调用对象对应的监视器锁,不会自动释放当前线程持有的其他锁。
3. notify() 和 notifyAll() 的区别
synchronized (LOCK) {
LOCK.notify();
}notify() 会从当前对象的等待集合中选择一个线程唤醒。
synchronized (LOCK) {
LOCK.notifyAll();
}notifyAll() 会唤醒当前对象等待集合中的全部线程。
但是:
唤醒不等于立即执行。
被唤醒的线程还需要竞争锁。
notify() 不保证唤醒哪个线程。
notifyAll() 不代表所有线程同时获得锁。
4. 为什么 wait() 必须配合 while?
不推荐:
synchronized (LOCK) {
if (!hasData) {
LOCK.wait();
}
// 处理数据
}原因:
线程可能发生虚假唤醒。
被唤醒时,等待条件可能已经不满足。
例如:
消费者 A、B 都在等待数据。
生产者生产一条数据,调用 notifyAll()。
A 和 B 都被唤醒。
A 先获得锁,消费数据。
B 再获得锁,此时已经没有数据。
如果使用 if,B 可能直接继续执行,造成业务错误。
推荐写法:
synchronized (LOCK) {
while (!hasData) {
LOCK.wait();
}
// 确认有数据,再处理
}线程被唤醒后,while 会再次判断业务条件。
如果条件不满足,就继续等待。
重点:wait() 应当放在 while 条件循环中,而不是使用 if 进行一次判断。
5. sleep() 和 wait() 的区别
注意:wait()、notify() 和 notifyAll() 都必须在持有对应对象监视器锁的情况下调用,否则抛出 IllegalMonitorStateException。
五、生产者消费者模型
生产者消费者模型是 wait() 和 notifyAll() 的典型应用。
假设一个消息容器:
生产者负责放入消息。
消费者负责取出消息。
容器一次只能保存一条消息。
容器有消息时,生产者等待。
容器没有消息时,消费者等待。
完整 Java 8 示例:
public class ProducerConsumerDemo {
static class MessageBox {
private String message;
private boolean hasMessage = false;
// 生产消息
public synchronized void put(String data)
throws InterruptedException {
// 容器已满,等待消费者
while (hasMessage) {
wait();
}
message = data;
hasMessage = true;
System.out.println("生产:" + data);
// 通知等待线程重新检查条件
notifyAll();
}
// 消费消息
public synchronized String take()
throws InterruptedException {
// 容器为空,等待生产者
while (!hasMessage) {
wait();
}
String result = message;
message = null;
hasMessage = false;
System.out.println("消费:" + result);
// 通知等待线程重新检查条件
notifyAll();
return result;
}
}
public static void main(String[] args)
throws InterruptedException {
MessageBox box = new MessageBox();
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
box.put("消息-" + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "producer");
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
box.take();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "consumer");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("处理结束");
}
}执行过程:
生产者 put()
↓
判断容器是否有消息
↓
有消息 → wait() 等待
无消息 → 放入消息
↓
notifyAll()
↓
消费者 take()
↓
判断容器是否有消息
↓
无消息 → wait() 等待
有消息 → 获取消息
↓
notifyAll()
↓
继续生产和消费需要注意:
synchronized保证同一时刻只有一个线程执行容器中的同步方法。wait()使不满足条件的线程等待,并释放容器的监视器锁。notifyAll()通知等待线程重新竞争锁并检查条件。while保证唤醒后再次检查业务条件。线程中断时,通过 InterruptedException 结束当前任务。
生产环境最佳实践:
对于生产者消费者模型,通常更推荐 JUC 提供的 BlockingQueue,例如:
BlockingQueue<String> queue =
new ArrayBlockingQueue<>(100);
// 生产者
queue.put("消息");
// 消费者
String message = queue.take();BlockingQueue 已经封装了线程安全和等待唤醒机制,不需要手动管理 wait()、notifyAll()。
以上均属于 Java 8 JDK 自带功能,无需引入第三方依赖。
六、重点总结
Java 使用协作式中断机制,
interrupt()不会强制终止线程。interrupt()用于请求中断指定线程。isInterrupted()只检查中断标记,不清除。Thread.interrupted()检查当前线程的中断标记,并清除。sleep()、wait()、join()因中断抛出 InterruptedException 时,会清除中断标记。捕获 InterruptedException 后,不应该无视异常;可以向上抛出,或者恢复中断标记并结束任务。
等待
synchronized锁的 BLOCKED 线程无法通过 interrupt() 直接退出锁等待。wait()会释放调用对象的监视器锁,notify()、notifyAll()不会立即释放锁。被 notify() 唤醒的线程必须重新获得锁才能继续执行。
wait()应该搭配while循环,防止虚假唤醒及业务条件变化。生产者消费者模型可以通过 wait/notifyAll 实现,但生产环境更推荐 BlockingQueue。
七、常见问题与答案
1. interrupt() 和 Thread.currentThread().interrupt() 有什么区别?
thread.interrupt() 中断指定线程。
Thread.currentThread().interrupt() 中断当前正在执行代码的线程,常用于捕获 InterruptedException 后恢复中断标记。
2. Thread.interrupted() 为什么调用后中断标记变成 false?
因为这是一个静态方法,它会检查并清除当前线程的中断标记。
3. 为什么 sleep() 被中断后,中断状态变成 false?
因为 sleep() 响应中断时会抛出 InterruptedException,并清除中断标记。
4. 捕获 InterruptedException 后,为什么需要重新调用 interrupt()?
如果当前方法不能向上抛出异常,而上层还需要感知中断请求,就应该恢复中断标记,防止中断信号丢失。
5. wait() 为什么要释放锁?
因为等待线程需要让出监视器锁,使其他线程能够获取锁并修改共享条件,从而满足等待线程继续执行的条件。
6. notify() 调用之后,线程会立即执行吗?
不会。notify() 只是发出唤醒通知,目标线程需要重新竞争并获得监视器锁才能继续执行。
7. 为什么 wait() 不能使用 if?
因为可能出现虚假唤醒,或者多个线程被唤醒后等待条件已经变化,使用 while 可以确保每次恢复执行时都重新检查条件。
8. 为什么生产环境更推荐 BlockingQueue?
因为 BlockingQueue 已经封装线程安全、等待唤醒以及队列容量控制机制,减少手动使用 wait/notify 导致的并发错误。
