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