线程管理

线程组(选择性看)

​ 线程组开始是出于安全的考虑设计用来区分不同的 Applet,然而ThreadGroup 并未实现这一目标,在新开发的系统中,已经不常用线程组, 现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分, 多数情况下,可以忽略线程组。

​ 线程组管理线程就类似于在计算机中使用文件夹管理文件, 在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组。

​ Thread 类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组。JVM 在创建 main 线程时会为它指定一个线程组,因此每个 Java 线程都有一个线程组与之关联,可以调用线程的 getThreadGroup()方法返回线程组。

创建线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThreadGroupTest {

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); // true
// 4 在创建线程时指定所属线程组
Runnable r = () -> 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);//Thread[t2,5,group1]
System.out.println(t3);//Thread[t3,5,group2]
}
}

线程组的基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @author D丶Cheng
* @Description: 线程组基本使用
* activeCount() 返回当前线程组及子线程组中活动线程的数量(近似值)
* activeGroupCount() 返回当前线程组及子线程组中活动线程组的数量(近似值)
* int enumerate(Thread[] list) 将当前线程组中的活动线程复制到参数数组中
* enumerate(ThreadGroup[] list) 将当前线程组中的活动线程组复制到参数数组中
* getMaxPriority() 返回线程组的最大优先级,默认是 10
* getName() 返回线程组的名称
* getParent() 返回父线程组
* interrupt() 中断线程组中所有的线程
* isDaemon() 判断当前线程组是否为守护线程组
* list() 将当前线程组中的活动线程打印出来
* parentOf(ThreadGroup g) 判断当前线程组是否为参数线程组的父线程组
* setDaemon(boolean daemon) 设置线程组为守护线程组
*/
public class ThreadGroupTest02 {

public static void main(String[] args) {
//返回当前线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
// 再定义线程组,默认 group 的父线程组是 main 线程组
ThreadGroup group = new ThreadGroup("group");
Runnable r = () -> {
while (true) {
System.out.println("-----------当前线程: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//默认在main
Thread t1 = new Thread(r, "t1");
//在指定的group线程组中创建线程
Thread t2 = new Thread(group, r, "t2");
t1.start();
t2.start();
//打印线程组的相关属性
//4, main 线程组中活动线程: main, t1, t2, 垃圾回收器
System.out.println("main线程组中活动线程数量: " + mainGroup.activeCount());
System.out.println("group子线程组中活动线程数量: " + group.activeCount());//1, t2
System.out.println("main线程组中子线程组数量: " + mainGroup.activeGroupCount()); //1, group
System.out.println("group子线程组中子线程组数量: " + group.activeGroupCount()); //0
//main线程组的父线程组是system
System.out.println("main线程组的父线程组: " + mainGroup.getParent());
System.out.println("group线程组的父线程组: " + group.getParent()); //main
//true, 线程组也是它自己的父线程组
System.out.println(mainGroup.parentOf(mainGroup));
System.out.println(mainGroup.parentOf(group)); //true
//把 main 线程组中所有的线程打印输出
mainGroup.list();
}
}

复制线程组中的线程及子线程组

​ enumerate(Thread[] list) 把当前线程组和子线程组中所有的线程复制到参数数组中

​ enumerate(Thread[] list, boolean recursive) ,如果第二个参数设置为false,则只复制当前线程组中所有的线程,不复制子线程组中的线程 。

​ enumerate(ThreadGroup[] list) 把当前线程组和子线程组中所有的线程组复制到参数数组中。

​ enumerate(ThreadGroup[] list, boolean recurse) 第二个参数设置false,则只复制当前线程组的子线程组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ThreadGroupTest03 {
public static void main(String[] args) {
//返回 main 线程的 main 线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
//main 线程组中定义了两个子线程组
//默认group1的父线程组就是当前线程组 main
ThreadGroup group1 = new ThreadGroup("group1");
ThreadGroup group2 = new ThreadGroup(mainGroup, "group2");
Runnable r = () -> {
while (true) {
System.out.println("----当前线程: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建并启动三个线程
//默认在 main 线程组中创建线程
Thread t1 = new Thread(r, "t1");
//在 group1 线程组中创建线程
Thread t2 = new Thread(group1, r, "t2");
//在 group2 线程组中创建线程
Thread t3 = new Thread(group2, r, "t3");
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);
}
}
}

捕获线程的执行异常

​ 在线程的run方法中,如果有受检异常必须进行捕获处理,如果想要获得run()方法中出现的运行时异常信息,可以通过回调UncaughtExceptionHandler接口获得哪个线程出现了运行时异常。在Thread 类中有关处理运行异常的方法有:

getDefaultUncaughtExceptionHandler() 获得全局的 ( 默认的)UncaughtExceptionHandler

getUncaughtExceptionHandler() 获得当前线程的UncaughtExceptionHandler

setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置全局的 UncaughtExceptionHandler

setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)设置当前线程的 UncaughtExceptionHandler

​ 当线程运行过程中出现异常,JVM会调用Thread类的dispatchUncaughtException(Throwable e)方法 , 该方法会调用getUncaughtExceptionHandler().uncaughtException(this, e) 如果想要获得线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class UnCaughtExceptionHandlerTest {

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(() -> {
System.out.println(Thread.currentThread().getName() + "开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) { //线程中的受检异常必须捕获处理
e.printStackTrace();
}
// 制造一个异常
System.out.println(12 / 0);
});
t1.start();
new Thread(() -> {
String txt = null;
//制造一个异常
System.out.println(txt.length());
}).start();
/*
在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异常执行的方法如果线程产生了异常,
JVM会调用dispatchUncaughtException()方法,在该方法中调用了getUncaughtExceptionHandler()
.uncaughtException(this, e); 如果当前线程设置了UncaughtExceptionHandler
回调接口就直接调用它自己的uncaughtException方法,如果没有设置则调用当前线程所在线程组
UncaughtExceptionHandler回调接口的uncaughtException方法,
如果线程组也没有设置回调接口,则直接把异常的栈信息定向到 System.err 中
*/
}
}

注入Hook钩子线程

​ 现在很多软件包括 MySQL, Zookeeper, kafka 等都存在 Hook 线程的校验机制,目的是校验进程是否已启动,防止重复启动程序。Hook 线程也称为钩子线程,当 JVM退出的时候会执行Hook线程。经常在程序启动时创建一个.lock 文件, 用.lock 文件校验程序是否启动,在程序退出(JVM 退出)时删除该.lock 文件,在 Hook 线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在 Hook 线程中进行复杂的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class HookTest {
public static void main(String[] args) {
//1 注入 Hook 线程,在程序退出时删除.lock 文件
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("JVM 退出,会启动当前Hook线程,在Hook线程中删除.lock文件");
getLockFile().toFile().delete();
}
});
//2 程序运行时,检查 lock 文件是否存在,如果 lock 文件存在,则抛出异常
if (getLockFile().toFile().exists()) {
throw new RuntimeException("程序已启动");
} else {
//文件不存在,说明程序是第一次启动,创建 lock 文件
try {
getLockFile().toFile().createNewFile();
System.out.println("程序在启动时创建了 lock 文件");
} catch (IOException e) {
e.printStackTrace();
}
}
//模拟程序运行
for (int i = 0; i < 10; i++) {
System.out.println("程序正在运行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private static Path getLockFile() {
return Paths.get("", "tmp.lock");
}
}