7.线程管理
- 7.1线程组
- 7.1.1 创建线程组
- 7.1.2 线程组的基本操作
- 7.1.3 复制线程组中的线程及子线程组
- 7.1.4 线程组的批量中断
- 7.1.5 设置守护线程组
- 7.2 捕获线程的执行异常
- 7.3 注入 Hook 钩子线程
7.1线程组
类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程,在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组。
Thread 类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组。
JVM 在创建 main 线程时会为它指定一个线程组,因此每个 Java 线程都有一个线程组与之关联,可以调用线程的 getThreadGroup()方法返回线程组。
线程组开始是出于安全的考虑设计用来区分不同的 Applet,然而ThreadGroup 并未实现这一目标,在新开发的系统中,已经不常用线程组,现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分,多数情况下,可以忽略线程组。
7.1.1 创建线程组
/**
* 演示创建线程组
*/
public class Test01 {
public static void main(String[] args) {
// 1) 返回当前 main 线程的线程组
ThreadGroup mainGroup =
Thread.currentThread().getThreadGroup();
System.out.println(mainGroup);
//2) 定义线程组,如果不指定所属线程组,则自动归属当前线程所属的线程组中
ThreadGroup group1 = new ThreadGroup("group1");
System.out.println(group1);
//3)定义线程组, 同时指定父线程组
ThreadGroup group2 = new ThreadGroup(mainGroup, "group2");
//现在 group1 与 group2 都是 maingroup 线程组中的子线程组, 调用线程组的 getParent()方法返回父线程组
System.out.println(group1.getParent() == mainGroup); //true
System.out.println(group2.getParent() == mainGroup);
//4) 在创建线程时指定所属线程组
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
};
//在创建线程时,如果没有指定线程组,则默认线程归属到父线程的线程组中
//在 main 线程中创建了 t1 线程,称 main 线程为父线程,t1 线程为子线程, t1 没有指定线程组则 t1 线程就归属到父线程 main 线程的线程组中
Thread t1 = new Thread(r, "t1");
System.out.println(t1); //Thread[t1,5,main], t1 的线程组是 main 线程组
//创建线程时,可以指定线程所属线程组
Thread t2 = new Thread(group1, r, "t2");
Thread t3 = new Thread(group2, r, "t3");
System.out.println(t2);
System.out.println(t3);
}
}
7.1.2 线程组的基本操作
// 返回当前线程组及子线程组中活动线程的数量(近似值)
activeCount()
// 返回当前线程组及子线程组中活动线程组的数量(近似值)
activeGroupCount()
// 将当前线程组中的活动线程复制到参数数组中
int enumerate(Thread[] list)
// 将当前线程组中的活动线程组复制到参数数组中
enumerate(ThreadGroup[] list)
// 返回线程组的最大优先级,默认是 10
getMaxPriority()
// 返回线程组的名称
getName()
// 返回父线程组
getParent()
// 中断线程组中所有的线程
interrupt()
// 判断当前线程组是否为守护线程组
isDaemon()
// 将当前线程组中的活动线程打印出来
list()
// 判断当前线程组是否为参数线程组的父线程组
parentOf(ThreadGroup g)
// 设置线程组为守护线程组
setDaemon(boolean daemon)
/**
* 演示线程组的基本操作
*/
public class Test02 {
public static void main(String[] args) {
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); //返回当前线程组
//再定义线程组
ThreadGroup group = new ThreadGroup("group"); //默认 group 的父线程组是 main线程组
Runnable r = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("-----------当前线程: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(r, "t1"); //默认在 main 线程组中创建线程
Thread t2 = new Thread(group, r, "t2"); //在指定的 group 线程组中创建线程
t1.start();
t2.start();
//打印线程组的相关属性
System.out.println("main 线 程组 中活 动线 程数 量 : " + mainGroup.activeCount());
//4, main 线程组中活动线程: main, t1, t2, 垃圾回收器
System.out.println("group 子 线 程 组 中 活 动 线 程 数 量 : " + group.activeCount());
//1, t2
System.out.println("main 线 程 组 中 子 线 程 组 数 量 : " +
mainGroup.activeGroupCount()); //1, group
System.out.println("group 子线程组中子线程组数量: " + group.activeGroupCount()); //0
System.out.println("main 线程组的父线程组: " + mainGroup.getParent()); //main线程组的父线程组是 system
System.out.println("group 线程组的父线程组: " + group.getParent()); //main
System.out.println(mainGroup.parentOf(mainGroup)); //true, 线程组也是它自己的父线程组
System.out.println(mainGroup.parentOf(group)); //true
mainGroup.list(); //把 main 线程组中所有的线程打印输出
}
}
7.1.3 复制线程组中的线程及子线程组
enumerate(Thread[] list) // 把当前线程组和子线程组中所有的线程复制到参数数组中
enumerate(Thread[] list, boolean recursive) // 如果第二个参数设置为 false,则只复制当前线程组中所有的线程,不复制子线程组中的线程
enumerate(ThreadGroup[] list) // 把当前线程组和子线程组中所有的线程组复制到参数数组中
enumerate(ThreadGroup[] list, boolean recurse) // 第二个参数设置false,则只复制当前线程组的子线程组
/**
* 演示复制线程组中的内容
*/
public class Test03 {
public static void main(String[] args) {
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); //返回 main线程的 main 线程组
//main 线程组中定义了两个子线程组
ThreadGroup group1 = new ThreadGroup("group1"); //默认 group1 的父线程组就是当前线程组 main
ThreadGroup group2 = new ThreadGroup(mainGroup, "group2");
Runnable r = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("----当前线程: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建并启动三个线程
Thread t1 = new Thread(r, "t1"); //默认在 main 线程组中创建线程
Thread t2 = new Thread(group1, r, "t2"); //在 group1 线程组中创建线程
Thread t3 = new Thread(group2, r, "t3"); //在 group2 线程组中创建线程
t1.start();
t2.start();
t3.start();
//1) 把 main 线程组中的线程复制到数组中
//先定义存储线程的数组,数组的长度为 main 线程组中活动线程的数量
Thread[] threadList = new Thread[mainGroup.activeCount()];
// //把 main 线程组包括子线程组中的所有的线程复制到数组中
// mainGroup.enumerate(threadList);
// //遍历 threadList 数组
// for (Thread thread : threadList) {
// System.out.println(thread);
// }
// System.out.println("----------------------------");
//只把 main 线程组中的线程复制到数组中,不包含子线程组的线程
mainGroup.enumerate(threadList, false);
//遍历 threadList 数组
for (Thread thread : threadList) {
System.out.println(thread);
}
System.out.println("----------------------------");
//2) 把 main 线程组中的子线程组复制到数组中
//定义数组存储线程组
ThreadGroup[] threadGroups = new ThreadGroup[mainGroup.activeGroupCount()];
//把 main 线程组中的子线程组复制到数组中
mainGroup.enumerate(threadGroups);
System.out.println("============================");
for (ThreadGroup threadGroup : threadGroups) {
System.out.println(threadGroup);
}
}
}
7.1.4 线程组的批量中断
线程组的 interrupt() 可以给该线程组中所有的活动线程添加中断标志。
/**
* 线程组的批量中断
*/
public class Test04 {
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("当前线程--" + Thread.currentThread() + "--开始循环");
//当线程没有被中断就一直循环
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() +
"------------------");
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// //如果中断睡眠中的线程,产生中断异常, 同时会清除中断标志
// e.printStackTrace();
// }
}
System.out.println(Thread.currentThread().getName() + "循环结束");
}
};
//创建线程组
ThreadGroup group = new ThreadGroup("group");
//在 group 线程组中创建 5 个线程
for (int i = 0; i < 5; i++) {
new Thread(group, r).start();
}
//main 线程睡眠 2 秒
Thread.sleep(50);
//中断线程组, 会中断线程组中所有的线程
group.interrupt();
}
}
7.1.5 设置守护线程组
守护线程是为其他线程提供服务的,当 JVM 中只有守护线程时,守护线程会自动销毁,JVM 会退出。
调用线程组的 setDaemon(true)可以把线程组设置为守护线程组,当守护线程组中没有任何活动线程时,守护线程组会自动销毁。
注意:
线程组的守护属性,不影响线程组中线程的守护属性,或者说守护线程组中的线程可以是非守护线程
/**
* 演示设置守护线程组
*/
public class Test05 {
public static void main(String[] args) throws InterruptedException {
//先定义线程组
ThreadGroup group = new ThreadGroup("group");
//设置线程组为守护线程组
group.setDaemon(true);
//向组中添加 3 个线程
for (int i = 0; i < 3; i++) {
new Thread(group, new Runnable() {
@Override
public void run() {
for (int j = 0; j < 20; j++) {
System.out.println(Thread.currentThread().getName() + " -- " + j);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//main 线程睡眠 5 秒
Thread.sleep(5000);
System.out.println("main...end....");
}
}
7.2 捕获线程的执行异常
在线程的run方法中,如果有受检异常必须进行捕获处理,如果想要获 得 run() 方法中出现的运行时异常信息,可以通过回调UncaughtExceptionHandler 接口获得哪个线程出现了运行时异常。
在Thread 类中有关处理运行异常的方法有getDefaultUncaughtExceptionHandler() 获得全局的 ( 默 认的)UncaughtExceptionHandler
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
return defaultUncaughtExceptionHandler;
}
getUncaughtExceptionHandler() 获得当前线程的UncaughtExceptionHandler
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;
}
setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置全局的 UncaughtExceptionHandler
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new RuntimePermission("setDefaultUncaughtExceptionHandler")
);
}
defaultUncaughtExceptionHandler = eh;
}
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)设置当前线程的 UncaughtExceptionHandler
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
当线程运行过程中出现异常,JVM 会调用 Thread 类的dispatchUncaughtException(Throwable e) 方法,该方法会调用getUncaughtExceptionHandler().uncaughtException(this, e); 如果想要获得线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
??:
/**
* 演示设置线程的 UnCaughtExceptionHandler 回调接口
*/
public class Test01 {
public static void main(String[] args) {
//1)设置线程全局的回调接口
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
//t 参数接收发生异常的线程, e 就是该线程中的异常
System.out.println(t.getName() + "线程产生了异常: " + e.getMessage());
}
});
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
//线程中的受检异常必须捕获处理
e.printStackTrace();
}
System.out.println(12 / 0); //会产生算术异常
}
});
t1.start();
new Thread(new Runnable() {
@Override
public void run() {
String txt = null;
System.out.println(txt.length()); //会产生空指针异常
}
}).start();
/*
在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异常执行的方
法
如果线程产生了异常, JVM 会调用 dispatchUncaughtException()方法,在该方法
中调用了 getUncaughtExceptionHandler().uncaughtException(this, e); 如果当前线程设置了
UncaughtExceptionHandler 回调接口就直接调用它自己的 uncaughtException 方法, 如果没有
设置则调用当前线程所在线程组 UncaughtExceptionHandler 回调接口的 uncaughtException 方
法,如果线程组也没有设置回调接口,则直接把异常的栈信息定向到 System.err 中
*/
}
}
7.3 注入 Hook 钩子线程
现在很多软件包括 MySQL、Zookeeper、kafka 等都存在 Hook 线程的校验机制,目的是校验进程是否已启动,防止重复启动程序。
Hook 线程也称为钩子线程,当 JVM 退出的时候会执行 Hook 线程。经常在程序启动时创建一个 .lock 文件,用 .lock 文件校验程序是否启动,在程序退出(JVM 退出)时删除该 .lock 文件,在 Hook 线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在 Hook 线程中进行复杂的操作。