Java 使用 ThreadGroup 来表示线程组,它可以对一批线程进行分类管理,Java 允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内,例如A线程创建了B线程,并且没有指定B线程的线程组,则B线程属于A线程所在的线程组。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
Thread 类提供了如下几个构造器来设置新创建的线程属于哪个线程组。
- Thread(ThreadGroup group, Runnable target):以 target 的 run() 方法作为线程执行体创建新线程,属于 group 线程组。
- Thread(ThreadGroup group, Runnable target, String name):以 target 的 run() 方法作为线程执行体创建新线程,该线程属于 group 线程组,且线程名为 name。
- Thread(ThreadGroup group, String name):创建新线程,新线程名为 name,属于 group 线程组。
因为中途不可改变线程所属的线程组,所以 Thread 类没有提供 setThreadGroup() 方法来改变线程所属的线程组(但提供了一个 getThreadGroup() 方法来返回该线程所属的线程组,getThreadGroup() 方法的返回值是 ThreadGroup 对象,表示一个线程组。ThreadGroup 类提供了如下两个简单的构造器来创建实例。
- ThreadGroup(String name):以指定的线程组名字来创建新的线程组。
- ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组。
上面两个构造器在创建线程组实例时都必须为指定一个名字。也就是说,线程组总会具有一个字符串类型的名字,该名字可通过调用ThreadGroup的 getName() 方法来获取,但不允许改变线程组的名字。 ThreadGroup 类提供了如下几个常用的方法来操作整个线程组的所有线程。
- int activeCount():返回此线程组中活动线程的数目。
- interrupt():中断此线程组中所有的线程。
- isDaemon():判断该线程组是否是后台线程组。
- setDaemon(boolean daemon):把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁。
- setMaxPriority(int pri):设置线程组的最高优先级。
下面程序创建几个线程,它们分别属于不同的线程组,程序还将一个线程组设置成后台线程。
class TestThread extends Thread{ //提供指定线程名的构造器 public TestThread(String name){ super(name); } //提供指定线程名、线程组的构造器 public TestThread(ThreadGroup group , String name){ super(group, name); } public void run(){ for (int i = 0; i < 20 ; i++ ){ System.out.println(getName() + " 线程的i变量" + i); } } } public class ThreadGroupTest{ public static void main(String[] args) { //获取主线程所在的线程组,这是所有线程默认的线程组 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); System.out.println("主线程组的名字:" + mainGroup.getName()); System.out.println("主线程组是否是后台线程组:" + mainGroup.isDaemon()); new TestThread("主线程组的线程").start(); ThreadGroup tg = new ThreadGroup("新线程组"); tg.setDaemon(true); System.out.println("tg线程组是否是后台线程组:" + tg.isDaemon()); TestThread tt = new TestThread(tg , "tg组的线程甲"); tt.start(); new TestThread(tg , "tg组的线程乙").start(); } }
上面程序中的第一段粗体字代码用于获取主线程所属的线程组,并访问该线程组的相关属性,第二段粗体字代码创建了一个新线程组,并将该线程组设置成后台线程。
ThreadGroup 内还定义了一个很有用的方法:void uncaughtException(Thread t , Throwable e),该方法可以处理该线程组内任意线程所抛出的未处理异常。
从 Java 5 开始, Java 加强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常, JVM 在结束该线程之前会自动查找是否有对应的 Thread.UncaughtExceptionHandler 对象,如果找到该处理器对象,则会调用该对象的 uncaughtException(Thread t, Throwable e) 方法来处理该异常。
Thread.UncaughtExceptionHandler 是 Thread 类的一个静态内部接口,该接口内只有一个方法: void uncaughtException(Thread t , Throwable e),该方法中的 t 代表出现异常的线程,而 e 代表该线程抛出的异常。
Thread 类提供了如下两个方法来设置异常处理器。
- static setDefaultUncaughtExceptionHandler (Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器。
- setUncaughtExceptionHandler (Thread.UncaughtExceptionHandler eh):为指定的线程实例设置异常处理器。
ThreadGroup 类实现了 Thread.UncaughtExceptionHandler 接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时, JVM 会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler() 方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM 将会调用该线程所属的线程组对象的 uncaughtException() 方法来处理该异常。
线程组处理异常的默认流程如下。
- 如果该线程组有父线程组,则调用父线程组的 uncaughtException() 方法来处理该异常。
- 如果该线程实例所属的线程类有默认的异常处理器(由 setDefaultUncaughtExceptionHandler() 方法设置的异常处理器),那么就调用该异常处理器来处理该异常。
- 如果该异常对象是 ThreadDeath 的对象,则不做任何处理;否则,将异常跟踪栈的信息打印到 System.err 错误输出流,并结束该线程。
下面程序为主线程设置了异常处理器,当主线程运行抛出未处理异常时,该异常处理器将会起作用。
class MyExHandler implements Thread.UncaughtExceptionHandler{ //实现uncaughtException方法,该方法将处理线程的未处理异常 public void uncaughtException(Thread t, Throwable e){ System.out.println(t + " 线程出现了异常:" + e); } } public class ExHandler{ public static void main(String[] args){ //设置主线程的异常处理器 Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler()); int a = 5 / 0; //①
System.out.println("程序正常结束!");
}
}
上面程序的主方法中粗体字代码为主线程设置了异常处理器,而①号代码处将引发一个未处理异常,则该异常处理器会负责处理该异常。运行该程序,会看到如下输出:
Thread [main, 5, main]线程出现了异常:java.lang.ArithmeticException: / by zero
从上面程序的执行结果来看,虽然程序中粗体字代码指定了异常处理器对未捕获的异常进行处理,而且该异常处理器也确实起作用了,但程序依然不会正常结束。这说明异常处理器与通过 catch 捕获异常是不同的——当使用 catch 捕获异常时,异常不会向上传播给上一级调用者;但使用异常处理器对异常进行处理之后,异常依然会传播给上一级调用者。