【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 操作,但它规避了编译器对受检异常的检查保障 —— 调用方法必须声明所有可能抛出的受检异常。

(即,这是一种不安全的错误代码模式。)

Java代码

 

  1. void sneakyThrow(Throwable t) {  

  2.   Thread.currentThread().stop(t);  

  3. }  

我该用什么方法替代 Thread.stop ?

绝大多数使用 stop 的代码应改为:

  • 修改一些标记性变量的值 以指示目标线程应停止运行;

  • 目标线程应定期检查这些标记变量;当检测到变量值指示应停止运行时,再有条不紊地从运行方法中自行返回。

  • 为了保证“停止请求”通信正确,此标记变量必须是 volatile 或 对其访问操作进行同步。

示例:假设你的程序中有以下三个方法 start、stop、run。

Java代码

 

  1. private Thread blinker;  

  2.   

  3. void start() {  

  4.   blinker = new Thread(this);  

  5.   blinker.start();  

  6. }  

  7.   

  8. void stop() {  

  9.   blinker.stop(); // 不安全的操作  

  10. }  

  11.   

  12. void run() {  

  13.   while (true) {  

  14.     try {  

  15.       Thread.sleep(interval);  

  16.     } catch (InterruptedException e) {  

  17.     }  

  18.     repaint();  

  19.   }  

  20. }  

那么你可以改造自己的 stop 和 run,避免使用 Thread.stop:

Java代码

 

  1. private volatile Thread blinker;  

  2.   

  3. void stop() {  

  4.   blinker = null;  

  5. }  

  6.   

  7. void run() {  

  8.   Thread thisThread = Thread.currentThread;  

  9.   while (blinker == thisThread) { // 每次迭代前先检查标记变量  

  10.     try {  

  11.       Thread.sleep(interval);  

  12.     } catch (InterruptedException e) {  

  13.       repaint();  

  14.     }  

  15.   }  

  16. }  

如何终止一个等待了很长时间的线程?(如,等待输入内容很久的线程)

Thread.interrupt 方法就是用来干这事的。

可以结合上述 “基于状态信号机制” 使用:状态变化后调用此方法,以中断等待。如:

Java代码

 

  1. void stop() {  

  2.   Thread moribund = waiter;  

  3.   waiter = null;  

  4.   moribund.interrupt();  

  5. }  

这项技术中,非常关键的一点是,如果方法捕获到中断异常,但又不想立即处理它,那就得 “再次声明” 此异常。(因为 InterruptedException 是受检异常。)

之所以说是 “再次声明” 而不是 “再次抛出”,是因为并不是所有情况下都能再次抛出此异常。

如果方法捕获了 InterruptedException 且未声明会抛出此异常,那就需要 “重新中断自己”:

Java代码

 

  1. 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 线程的状态:

Java代码

 

  1. boolean threadSuspended;  

  2. void mousePressed(MouseEvent e) {  

  3.   e.consume();  

  4.   

  5.   if (threadSuspended) {  

  6.     blinker.resume();  

  7.   } else {  

  8.     blinker.suspened(); // 容易引发死锁  

  9.   }  

  10.   

  11.   threadSuspended = !threadSuspended;  

  12. }  

你可以改造为如下形式:

Java代码

 

  1. volatile boolean threadSuspended;  

  2.   

  3. synchronized void mousePressed(MouseEvent e) {  

  4.   e.consume();  

  5.   

  6.   threadSuspended = !threadSuspended;  

  7.   

  8.   if (!threadSuspended) {  

  9.     notify();  

  10.   }  

  11. }  

  12.   

  13. void run() {  

  14.   while (true) {  

  15.     try {  

  16.       Thread.sleep(interval);  

  17.   

  18.       if (threadSuspended) {  

  19.         synchronized(this) {  

  20.           while (threadSuspended) {  

  21.             wait();  

  22.           }  

  23.         }  

  24.       }  

  25.     } catch (InterruptedException e) {  

  26.     }  

  27.     repaint();  

  28.   }  

  29. }  

  • run 方法就是 blinker 线程运行的内容。

  • notify 和 wait 方法都被包在 synchronized 块中,保证它们的正确的执行顺序。这样可以避免因被挂起线程遗漏 notify 信号而导致无限挂起。

  • 标记变量 threadSuspended 是 volatile,以保证挂起请求的正常通信。

  • run 方法中的利用 双检锁 的形式来减少不必要的同步。

我能利用上述两个技术创建一个可以被 安全地 “stopped” 和 “suspended” 的线程吗?

可以,而且很简单。

比较微妙的是,另一个线程尝试停止目标线程时,目标线程可能已经处于挂起状态。

如果你的 stop 方法仅仅是更改 状态变量的值,那么目标线程是继续挂起(在monitor上等待),而不是期望的“优雅退出”。这会导致程序行为不稳定。

为了矫正这种状况,stop 方法必须确保目标线程能从挂起状态立即恢复。

目标线程一旦恢复,必须能立即识别出自己已被标记为“需要终止”。

实现示例:

Java代码

 

  1. private volatile Thread blinker;  

  2.   

  3. void run() {  

  4.   Thread thisThread = Thread.currentThread();  

  5.   while (blinker == thisThread) { // 判断是否“需要终止”  

  6.     try {  

  7.       Thread.sleep(interval);  

  8.   

  9.       synchronized(this) {  

  10.         // 只有当 不需要终止 且 期望挂起 时才会执行 wait  

  11.         while (threadSuspended && blinker==thisThread) {  

  12.           wait();  

  13.         }  

  14.       }  

  15.     } catch (InterruptedException e) {  

  16.     }  

  17.     repaint();  

  18.   }  

  19. }  

  20.   

  21. synchronized void stop() {  

  22.   blinker = null; // 会被目标线程判定为“需要终止”  

  23.   notify();  // 立即唤醒目标线程  

  24. }  

当然,你也可以在 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的防御肯定是不切实际的。

发布了219 篇原创文章 · 获赞 3 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/hchaoh/article/details/103907661