[译]SEI CERT Oracle Coding Standard for Java - Thread Apis(THI01-J)

原文:https://wiki.sei.cmu.edu/confluence/display/java/THI01-J.+Do+not+invoke+ThreadGroup+methods

THI01-J. Do not invoke ThreadGroup methods

在Java语言里,每个线程在创建时都会被分配到一个线程组,这些线程组都是java.lang.ThreadGroup的一个实例,如果没有显式地给一个线程组命名,JVM会分配一个默认的线程组,ThreadGroup提供了一系列便利的方法,调用这些方法可以同时作用于该线程组中所有的线程。比如,ThreadGroup.interrupt()方法可以中断线程组中的所有线程。同时,强制的把线程分组,可以避免不同组的线程相互干扰,有助于构建层次化的安全架构[JavaThreads 2004]
虽然线程组对于管理线程很有用,但开发者却很难从中获利,原因在于ThreadGroup提供的很多方法都被废弃了,比如allowThreadSuspension(),resume(),stop(),suspend(),不仅如此,那些没有废弃的方法也没有多大实用价值。更讽刺的是,ThreadGroup提供的少数方法甚至不是线程安全的[Bloch 2001]
没有废弃但不安全的方法包括:

  • ThreadGroup.activeCount()
    根据Java API[API 2014]的描述,这个方法返回当前线程组及其子线程组中活动线程数的估算值,这个方法经常作为遍历一个线程组的先决条件。事实上,线程池中从未启动的那些线程也会被算作活动线程,同时,这个估算值还会受到某些系统线程的影响[API 2014]。因此,activeCount()并不能准确反映线程组当前的活动线程数。
  • ThreadGroup.enumerate() 根据Java API[API 2014]的描述,enumerate()会将当前线程组及其子线程组中的活动线程复制到一个列表返回,这个方法可能会根据activeCount()的估算结果分配数组大小,如果由于估算不准确导致数组太小,那么多出的超出线程就会被忽略。

使用ThreadGroup提供的方法关闭线程也存在陷阱,因为stop()方法已经废弃了,开发者需要通过其他方式结束一个线程,根据Java Programming Language[JPL 2006]:

One way is for the thread initiating the termination to join the other threads and so know when those threads have terminated. However, an application may have to maintain its own list of the threads it creates because simply inspecting the ThreadGroup may return library threads that do not terminate and for which join will not return.

Executor框架提供了更好地API用于管理线程组,使用了更安全的方式处理线程关闭和线程相关的异常处理[Bloch 2008].总之,我们不要在代码中调用ThreadGroup提供的方法。

不规范代码示例

在这个示例中,NetworkHandler类持有一个controller线程,controller线程将请求代理给 NetworkHandler的工作线程,为了验证竞态条件下的情况,NetworkHandler会相继启动3个工作线程,所有工作线程都分配到Chief线程组。

final class HandleRequest implements Runnable {
  public void run() {
    // Do something
  }
}
 
public final class NetworkHandler implements Runnable {
  private static ThreadGroup tg = new ThreadGroup("Chief");
 
  @Override public void run() {
    new Thread(tg, new HandleRequest(), "thread1").start();
    new Thread(tg, new HandleRequest(), "thread2").start();
    new Thread(tg, new HandleRequest(), "thread3").start();
  }
 
  public static void printActiveCount(int point) {
    System.out.println("Active Threads in Thread Group " + tg.getName() +
        " at point(" + point + "):" + " " + tg.activeCount());
  }
 
  public static void printEnumeratedThreads(Thread[] ta, int len) {
    System.out.println("Enumerating all threads...");
    for (int i = 0; i < len; i++) {
      System.out.println("Thread " + i + " = " + ta[i].getName());
    }
  }
 
  public static void main(String[] args) throws InterruptedException {
    // Start thread controller
    Thread thread = new Thread(tg, new NetworkHandler(), "controller");
    thread.start();
 
    // Gets the active count (insecure)
    Thread[] ta = new Thread[tg.activeCount()];
 
    printActiveCount(1); // P1
    // Delay to demonstrate TOCTOU condition (race window)
    Thread.sleep(1000);
    // P2: the thread count changes as new threads are initiated
    printActiveCount(2); 
    // Incorrectly uses the (now stale) thread count obtained at P1
    int n = tg.enumerate(ta); 
    // Silently ignores newly initiated threads
    printEnumeratedThreads(ta, n);
                                   // (between P1 and P2)
 
    // This code destroys the thread group if it does
    // not have any live threads
    for (Thread thr : ta) {
      thr.interrupt();
      while(thr.isAlive());
    }
    tg.destroy();
  }
}

这种方式存在“检查时间/使用时间问题”(TOCTOU)的缺陷,获取活动线程数和枚举活动线程这两个操作不能保证原子性,如果在调用activeCount()enumerate()这两个方法之间某时刻,有新的请求进入,实际上总的线程数增加了,但活动线程列表ta的长度是上一次调用activeCount()的取值,因此枚举活动线程不会包含新增的线程。
后续对活动线程数列表ta的使用也是不安全的,比如,我们想调用destroy()方法销毁线程组中所有线程,调用destroy()方法的前置条件是线程组中没有处于工作中的线程,以上代码实例中,对于所有的活动线程,尝试调用interrupt()方法中断,最后,调用线程组的destroy()方法,然而,事实上此时线程组中仍然存在活动线程,调用destroy()方法会导致抛出java.lang.IllegalThreadStateException

规范的代码

针对上述场景,正确的方式是使用一个固定长度的线程池来管理工作线程,java.util.concurrent.ExecutorService接口提供了一系列方法管理线程池,尽管ExecutorService没有提供获取活动线程数和枚举活动线程的方法,但可以通过它提供的方法方法控制一个逻辑线程组的行为,如以下代码所示,可以使用shutdownPool()方法终止特定线程池中所有线程。

public final class NetworkHandler {
  private final ExecutorService executor;
 
  NetworkHandler(int poolSize) {
    this.executor = Executors.newFixedThreadPool(poolSize);
  }
 
  public void startThreads() {
    for (int i = 0; i < 3; i++) {
      executor.execute(new HandleRequest());
    }
  }
 
  public void shutdownPool() {
    executor.shutdown();
  }
 
  public static void main(String[] args)  {
    NetworkHandler nh = new NetworkHandler(3);
    nh.startThreads();
    nh.shutdownPool();
  }
}

Java SE 5.0之前,如果需要捕获单个线程中抛出的异常,必须要使用ThreadGroup,因为这是直接提供这种功能的唯一方式。具体来说,UncaughtExceptionHandler只能通过继承ThreadGroup获得。在最近的java版本中,UncaughtExceptionHandler通过Thread类提供的内部接口Thread.UncaughtExceptionHandler被每个线程持有。总之,现在ThreadGroup类几乎没有包含不可替代的功能。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Oct 30, 2017

奉献爱心