【Java】被废弃的线程方法
原文:《Java Thread Primitive Deprecation》
为什么 Thread.stop 被废弃了?
Thread.stop 不安全。
- Stop 一个线程会导致它释放所持有的锁(monitor)。(ThreadDeatch 抛出后会释放 monitor 的锁。)
- 这可能会导致之前被 monitor 保护的对象处于 不一致的状态,并被其它线程看到。也就是说,这些 对象被损坏 了。
- 线程操作被损坏的对象会导致不可预料的行为。
- 这些行为可能很微妙难以被察觉,也可能非常明显。与其它异常不同的是,ThreadDeatch 异常会悄悄地杀掉线程,用户感知不到程序已被损坏。这种程序损坏可能在错误发生后任何时候显现,甚至是几小时、几天。
为什么 我不能捕获 ThreadDeath 异常,并修复损坏的对象?
理论上,也许是可以的。但这会使编写多线程任务代码非常复杂,几乎不可能实现。因为:
- 线程可能在任何地方抛出 ThreadDeatch 异常。必须非常详尽地分析所有同步方法和代码块,来处理这些情况。
- 线程在处理第一个 ThreadDeatch 异常期间(catch 或 finally),可能会再次抛出 ThreadDeatch 异常(嵌套异常)。你的异常处理操作必须不断重复,直到成功。这会导致代码非常复杂。
所以这种方案是不切实际的!
Thread.stop(Throwable) 如何?
上述提到的所有问题它都有。此外,此方法一般用于生成一个 目标线程未准备好处理的 异常(包括目标线程不可能抛出的一些 受检异常)。如,以下示例中方法的行为等价于 throw 操作,但它规避了编译器对受检异常的检查保障 —— 调用方法必须声明所有可能抛出的受检异常。
(即,这是一种不安全的错误代码模式。)
void sneakyThrow(Throwable t) { Thread.currentThread().stop(t); }
我该用什么方法替代 Thread.stop ?
绝大多数使用 stop 的代码应改为:
- 修改一些标记性变量的值 以指示目标线程应停止运行;
- 目标线程应定期检查这些标记变量;当检测到变量值指示应停止运行时,再有条不紊地从运行方法中自行返回。
- 为了保证“停止请求”通信正确,此标记变量必须是 volatile 或 对其访问操作进行同步。
示例:假设你的程序中有以下三个方法 start、stop、run。
private Thread blinker; void start() { blinker = new Thread(this); blinker.start(); } void stop() { blinker.stop(); // 不安全的操作 } void run() { while (true) { try { Thread.sleep(interval); } catch (InterruptedException e) { } repaint(); } }
那么你可以改造自己的 stop 和 run,避免使用 Thread.stop:
private volatile Thread blinker; void stop() { blinker = null; } void run() { Thread thisThread = Thread.currentThread; while (blinker == thisThread) { // 每次迭代前先检查标记变量 try { Thread.sleep(interval); } catch (InterruptedException e) { repaint(); } } }
如何终止一个等待了很长时间的线程?(如,等待输入内容很久的线程)
Thread.interrupt 方法就是用来干这事的。
可以结合上述 “基于状态信号机制” 使用:状态变化后调用此方法,以中断等待。如:
void stop() { Thread moribund = waiter; waiter = null; moribund.interrupt(); }
这项技术中,非常关键的一点是,如果方法捕获到中断异常,但又不想立即处理它,那就得 “再次声明” 此异常。(因为 InterruptedException 是受检异常。)
之所以说是 “再次声明” 而不是 “再次抛出”,是因为并不是所有情况下都能再次抛出此异常。
如果方法捕获了 InterruptedException 且未声明会抛出此异常,那就需要 “重新中断自己”:
Thread.currentThread().interrupt();
这样就可以再次让线程抛出 InterruptedException。
如果线程没有响应 Thread.interrupt 该怎么办?
在某些情况下,你可以使用一些特定于应用程序的技巧。例,如果线程正在等待一个 socket,你可以将该 socket 关闭,从而让线程立即返回。
不幸的是,这方面真的没有通用的技术。
需要说明的是,在所有场景中,如果一个等待线程没有响应 Thread.interrupt,那么它也不会响应 Thread.stop。如,恶意的拒绝服务攻击(DoS),某些 stop/interrupt 无法正确工作的 IO操作 等。
为什么 Thread.suspend 和 Thread.resume 被废弃了?
Thread.suspend 容易引发死锁。
如果目标线程持有某项资源的锁,然后被挂起(suspend),那么在它被恢复前(resume)其它线程无法访问该资源。
此时,如果那个可以恢复目标线程的另一个线程需要 “先获取该资源的锁 - 再恢复目标线程”,那么就形成了死锁。
这种死锁通常表现为“冻结”进程。
我该用什么替代 Thread.suspend 和 Thread.resume ?
类似 Thread.stop,谨慎的做法是:
- 设置一个标记性变量来表示期望的线程状态;
- 目标线程轮询该变量;当期望状态是“挂起”时,线程通过 Object.wait 方法将自己挂起等待;
- 当期望目标线程继续运行时,通过 Object.notify 方法唤醒它。
例,你的程序中有一个事件处理方法 mousePressed 用于切换 blinker 线程的状态:
boolean threadSuspended; void mousePressed(MouseEvent e) { e.consume(); if (threadSuspended) { blinker.resume(); } else { blinker.suspened(); // 容易引发死锁 } threadSuspended = !threadSuspended; }
你可以改造为如下形式:
volatile boolean threadSuspended; synchronized void mousePressed(MouseEvent e) { e.consume(); threadSuspended = !threadSuspended; if (!threadSuspended) { notify(); } } void run() { while (true) { try { Thread.sleep(interval); if (threadSuspended) { synchronized(this) { while (threadSuspended) { wait(); } } } } catch (InterruptedException e) { } repaint(); } }
- run 方法就是 blinker 线程运行的内容。
- notify 和 wait 方法都被包在 synchronized 块中,保证它们的正确的执行顺序。这样可以避免因被挂起线程遗漏 notify 信号而导致无限挂起。
- 标记变量 threadSuspended 是 volatile,以保证挂起请求的正常通信。
- run 方法中的利用 双检锁 的形式来减少不必要的同步。
我能利用上述两个技术创建一个可以被 安全地 “stopped” 和 “suspended” 的线程吗?
可以,而且很简单。
比较微妙的是,另一个线程尝试停止目标线程时,目标线程可能已经处于挂起状态。
如果你的 stop 方法仅仅是更改 状态变量的值,那么目标线程是继续挂起(在monitor上等待),而不是期望的“优雅退出”。这会导致程序行为不稳定。
为了矫正这种状况,stop 方法必须确保目标线程能从挂起状态立即恢复。
目标线程一旦恢复,必须能立即识别出自己已被标记为“需要终止”。
实现示例:
private volatile Thread blinker; void run() { Thread thisThread = Thread.currentThread(); while (blinker == thisThread) { // 判断是否“需要终止” try { Thread.sleep(interval); synchronized(this) { // 只有当 不需要终止 且 期望挂起 时才会执行 wait while (threadSuspended && blinker==thisThread) { wait(); } } } catch (InterruptedException e) { } repaint(); } } synchronized void stop() { blinker = null; // 会被目标线程判定为“需要终止” notify(); // 立即唤醒目标线程 }
当然,你也可以在 stop 方法中调用 Thread.interrupt 来终止目标线程,这样就不需要调用 notify。
但 interrupt 方法也必须包含到 synchronized 中,确保目标线程不会错过 interrupt 信号。
Thread.destroy 如何?
Thread.destroy 从来都没有被实现过,且已经被废弃了。如果它被实现了,那么它会像 Thread.suspend 那样容易引发死锁。(事实上,它相当于 Thread.suspend,且没有后续的 Thread.resume)
为什么 Runtime.runFinalizersOnExit 被废弃了?
Runtime.runFinalizersOnExit 不安全。
它可能会导致存活对象被finalize,且同时有其它线程正在操作这些对象,导致不稳定的行为或死锁。
虽然可以通过 Class 实现来避免存活对象被finalize,但最大多数程序员都不会怎么设计。他们都会假设对象被finalize时肯定已经死了。
而且此方法是设置 JVM 的一个标记变量,它会影响到所有类。让所有类的设计者都增加避免过早被finalize的防御肯定是不切实际的。
上一篇: 【Java 8 GC 调优】并行GC