SEI-CERT Java编程规范(Thread Apis)-谨慎使用ThreadGroup提供的方法

原文: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](中文引自《Java程序设计语言(第四版)》陈昊鹏等译):

让线程启动终止过程的的一种方式是让它join其他线程,这样可以知道加入的线程将在何时终止,然而,应用程序可能必须维护他自己创建的线程列表,因为直接检查ThreadGroup可以返回那些没有终止的库线程,而join却不返回这些库线程。

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类几乎没有包含不可替代的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值