为什么 Java 要废弃 Thread.stop()?看完这篇你就懂了

昨儿我写了一篇文章,主要回顾 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 然后修复不行吗?

理论上确实可以,但实际上几乎不可能写对。

原因有两个:

  1. 线程几乎可以在任何地方抛出 ThreadDeath,所有同步方法和同步块都得逐行检查,确保能正确处理这个异常。没有人能确保你在任何地方都能检查这个异常。
  2. 线程在清理第一个 ThreadDeath 的过程中(catchfinally 里),可能又抛出第二个。清理代码必须能反复重试直到成功,这会让代码变得极其复杂。

总之,不现实。

这个有点像以前的 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 还好一点,感觉你可以穷举加上,但是这个,你想 catchcatch 不住。

正确做法:自己控制标志位

大多数场景下,应该用一个标志变量来通知线程"该停了"。目标线程定期检查这个变量,收到信号后有序退出。

关键点:这个变量必须是 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() 就可靠地停止。例如 stopinterrupt 都无法正常工作的 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();
    }
}

注意 notifywait 都在 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 希望你用一种更安全的方式去行使这个控制权。

相关推荐
苦瓜花1 小时前
【Android】三大动画的实践
android
Mars-xq1 小时前
VSCode 开发 Android 时,类、方法无法跳转
android·ide·vscode
2601_961766642 小时前
【分享】Resprite安卓版|专业像素绘画,游戏美术创作工具
android·游戏美术
Mars-xq2 小时前
VSCode 开发Android 新手必装插件清单
android·ide·vscode
Wonderful U2 小时前
Python+Django实战|社区物业管理系统:业主档案、车位管理、物业费收缴、线上报修、投诉建议、园区公告、日常巡检
android·python·django
唐青枫2 小时前
Kotlin run 详解:把对象操作收进作用域,再把结果带出来
kotlin
三少爷的鞋3 小时前
现代 Android 官方为什么更推荐 Repository 暴露 `suspend fun`,而不是在内部 `launch`
android
黄林晴14 小时前
Google Play 发版链路全面重构:合规前置、审核自动化、生态全面收紧
android·google
通玄16 小时前
Jetpack Compose 入门系列(四):动画基本使用
android