
昨儿我写了一篇文章,主要回顾 Andorid 这么多年来作用的线程相关的技术有哪些。
当时在写那篇文章的时候,我突然发现 Thread 已经不让 stop 了,不仅如此,以前的 suspend()、resume()、destroy() 这些方法都标记为移除了。
如果你看 Java 8 的文档,对,没错,就是那个"版本任你发,我用 Java 8 "的那个版本:


这些函数就已经标记为 @Deprecated 了。
如果你看比较新的 Java 17:


Java 文档已经明确告诉你,这些函数就已经准备移除了。
甚至在 JDK 20 之后,这些函数已经被降级为直接抛 UnsupportedOperationException。
可能很多开发者已经知道了"不该用",但不一定清楚背后的原理。
这篇文章就来逐一拆解:它们为什么不安全,以及替代方案是什么。
为什么废弃
因为 Thread.stop() 本质上就是不安全的。
调用 stop() 会强制目标线程抛出 ThreadDeath 异常。异常沿调用栈向上传播的过程中,线程持有的所有监视器锁会被自动释放。
等等,你可能还没有意识到这个 stop() 有多暴力!
Java
class BankAccount {
private int balance = 100;
private int debt = 50;
// 转账操作:同时修改两个字段
synchronized void transfer(int amount) {
balance -= amount; // 第一步:扣余额
// ---- 假设线程在这里被 stop() 杀死 ----
debt += amount; // 第二步:加欠款
}
synchronized int getNetWorth() {
return balance - debt;
}
}
此时 JVM 并不会等待线程自己退出,而是直接在目标线程的执行位置插入一个异常:
java
throw new ThreadDeath();
当然这不是 Java 代码层面的插入,而是 JVM 内部完成的,具体原理这里不展开。
你可能一眼就看出问题了:如果被这些锁保护的对象当时正处于不一致的状态,锁一释放,其他线程就能看到这些"损坏"的对象,从而产生不可预测的行为。
更麻烦的是,ThreadDeath 和其他未检查异常不同,它是一个 Error 而非普通异常,且默认情况下如果未被捕获,线程会静默终止(不打印堆栈跟踪)。
如果你去看 stop 的文档,其中还有这样一段描述:
应用程序通常不应捕获
ThreadDeath异常,除非需要执行特殊的资源清理操作(注意:抛出ThreadDeath时,线程正式消亡前会先执行try代码块对应的finally语句)。若catch块捕获到ThreadDeath对象,必须重新抛出该异常对象,线程才能真正终止。
捕获异常呢
那么问题来了,捕获 ThreadDeath 然后修复不行吗?
理论上确实可以,但实际上几乎不可能写对。
原因有两个:
- 线程几乎可以在任何地方抛出
ThreadDeath,所有同步方法和同步块都得逐行检查,确保能正确处理这个异常。没有人能确保你在任何地方都能检查这个异常。 - 线程在清理第一个
ThreadDeath的过程中(catch或finally里),可能又抛出第二个。清理代码必须能反复重试直到成功,这会让代码变得极其复杂。
总之,不现实。
这个有点像以前的 IO 代码,如果你要 try-catch IO 异常,那么在 catch 中你需要关闭这个 IO,但是关闭/释放 IO 本身也有可能抛出异常。
我当时写这种 IO 代码写的我人都要疯了。
还有另一个 stop
好巧不巧,Thread 还有另一个 stop:
Java
public final void stop(Throwable obj)
我用这个不行吗?
这个其实更离谱!
我很少将一个技术称之为离谱,但是这个也太离谱了。
它允许你向线程异步注入任意 Throwable。
例如:
java
thread.stop(new RuntimeException("Boom"));
效果类似于:
java
// 在目标线程当前执行位置
throw new RuntimeException("Boom");
同样,这也不是真正插入代码,而是 JVM 在目标线程下一次安全检查点(Safepoint)时完成异常注入。
这样做就完全绕过了编译器对受检异常的检查。用它可以把任何异常"偷偷"抛给另一个线程,破坏 Java 的类型安全。
之前那个 stop 还好一点,感觉你可以穷举加上,但是这个,你想 catch 都 catch 不住。
正确做法:自己控制标志位
大多数场景下,应该用一个标志变量来通知线程"该停了"。目标线程定期检查这个变量,收到信号后有序退出。
关键点:这个变量必须是 volatile 的,或者对它的访问必须是同步的,否则线程可能看不到变更。
这里先来一个反面示例(不安全):
java
private Thread blinker;
public void stopTask() {
blinker.stop(); // 不安全!
}
public void run() {
while (true) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {}
repaint();
}
}
正确写法应该是这样:
java
private volatile Thread blinker;
public void stopTask() {
blinker = null;
}
public void run() {
Thread thisThread = Thread.currentThread();
while (blinker == thisThread) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {}
repaint();
}
}
通过将 blinker 设为 null,线程在下一次循环检查时就会自行退出。
停止一个长时间等待的线程
上面的方案适用于线程在循环中工作的场景。
但如果线程在等待输入(比如阻塞在 I/O 上),光靠标志变量就不够了。
这时候需要 Thread.interrupt()。在设置标志变量之后,再调用 interrupt() 中断阻塞:
java
public void cancelTask() {
Thread moribund = waiter;
waiter = null;
moribund.interrupt();
}
一个关键细节:如果某个方法捕获了 InterruptedException 但没有准备好立即处理,它必须"重新断言"这个异常。
此处的重新断言并不是重新抛出的意思,虽然重新抛出是个好方法,但是并不是所有情况下,你都能重新抛出异常。
如果方法签名上没有声明抛出 InterruptedException,或者无法重新抛出异常,可以用这种方式重新中断自己:
java
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断标记
return; // 或者做清理后退出
}
之所以需要这么做,是因为 InterruptedException 被抛出时,线程的中断标记通常会被清除。所以如果你捕获了它但不能继续抛出,就应该调用 Thread.currentThread().interrupt() 把中断标记补回去。
如果线程不响应 interrupt 呢
有些情况下可以用应用层面的技巧,比如线程在等待一个已知的套接字,直接关闭套接字就能让线程返回。
但确实没有通用方案。
需要注意一点:如果一个等待操作不响应 interrupt(),通常它也不会因为你调用 Thread.stop() 就可靠地停止。例如 stop 和 interrupt 都无法正常工作的 I/O 操作。
所以不要把 stop() 当成兜底方案。它不是更强大的取消机制,而是更危险的破坏机制。
为什么 suspend 和 resume 被废弃
这里的 suspend 可不是 Kotlin 协程的 suspend。

Thread.suspend() 本质上容易导致死锁。
如果线程在被挂起时持有某个关键资源的锁,那么在它被恢复之前,没有任何线程能访问这个资源。
而如果要恢复它的线程恰好需要先获取这个锁,就会死锁。
这种死锁的典型表现就是进程"卡死"。
正确做法:用 wait/notify
和上面的 stopTask 方法一样,正确思路是让目标线程通过轮询一个状态变量来决定是否暂停,也就是用 wait/notify 替代 suspend/resume
当需要暂停时,用 Object.wait() 等待;需要恢复时,用 Object.notify() 唤醒。
举个反面示例(容易死锁):
java
private boolean threadSuspended;
public void mousePressed(MouseEvent e) {
e.consume();
if (threadSuspended)
blinker.resume();
else
blinker.suspend(); // 容易死锁!
threadSuspended = !threadSuspended;
}
使用 wait/notify 的正确写法 ------ 事件处理器:
java
public synchronized void mousePressed(MouseEvent e) {
e.consume();
threadSuspended = !threadSuspended;
if (!threadSuspended)
notify();
}
运行循环中加入等待逻辑:
java
public void run() {
while (true) {
try {
Thread.sleep(interval);
synchronized(this) {
while (threadSuspended)
wait();
}
} catch (InterruptedException e) {}
repaint();
}
}
注意 notify 和 wait 都在 synchronized 块中,这是语言要求的,确保了两者被正确串行化,避免竞态条件导致线程错过唤醒信号。
性能优化
同步有开销。一个优化技巧是:只有在线程确实被挂起时才进入同步块,同时把 threadSuspended 声明为 volatile:
java
private volatile boolean threadSuspended;
public void run() {
while (true) {
try {
Thread.sleep(interval);
if (threadSuspended) {
synchronized(this) {
while (threadSuspended)
wait();
}
}
} catch (InterruptedException e) {}
repaint();
}
}
同时支持安全停止和挂起
两种技术可以组合使用,但有一个微妙之处:当另一个线程调用 stopTask 时,目标线程可能正处于挂起状态(在 wait() 上阻塞)。
如果 stopTask 只是把 blinker 设为 null,线程会继续挂在那里,而不是正常退出。
解决办法:stopTask 方法必须先唤醒挂起的线程,让它有机会检查停止标志:
java
public void run() {
Thread thisThread = Thread.currentThread();
while (blinker == thisThread) {
try {
Thread.sleep(interval);
synchronized(this) {
while (threadSuspended && blinker == thisThread)
wait();
}
} catch (InterruptedException e) {}
repaint();
}
}
public synchronized void stopTask() {
blinker = null;
notify();
}
如果 stopTask 方法使用了 Thread.interrupt,就不需要同时调用 notify,但仍然必须是同步的,以确保目标线程不会因为竞态条件而错过中断。
destroy 呢
Thread.destroy 从未被实现,直接废弃了。
如果实现了,它会像 Thread.suspend 一样容易死锁 ------ 实际上它大致等同于 suspend 但没有对应的 resume。
一点想法
看这几个被废弃的方法,会发现它们有一个共同的问题:让一个线程可以强行控制另一个线程的执行状态。
stop() 是强行杀,suspend() 是强行挂起,resume() 是强行恢复。听起来很方便 ------ 我想让你停你就停,想让你跑你就跑。
但问题是,被控制的线程完全不知道自己正在被操控,它的锁、它的状态、它正在执行的清理逻辑,随时可能被打断。
这就像你正在写一份文档,别人直接把你的电源拔了。文档可能写了一半,锁可能没释放,状态已经乱了。
Java 设计者最终给出的答案是:把控制权还给线程自己 ,或者说,把控制权还给了开发者。
对比一下就很清楚:
java
// 旧做法:外部强行停线程
thread.stop();
// 新做法:线程自己决定什么时候停
volatile boolean running = true;
// 外部只负责通知
running = false;
// 线程内部自己检查
while (running) {
// 干活
}
stop() 是"我不管你正在干嘛,给我停下来"。volatile 标志是"我通知你一下,你自己找个合适的时候停"。
suspend()/resume() 也是一样,推荐替换成 wait/notify。外部不再直接操控线程的状态,而是通过共享变量协作。
Java 废弃的不是"控制线程"的能力,而是"不打招呼就强行控制"的方式。最终控制权还是在开发者手里,只不过,Java 希望你用一种更安全的方式去行使这个控制权。