欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Java】被废弃的线程方法

程序员文章站 2022-05-30 10:57:32
...

原文:《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的防御肯定是不切实际的。