Java - ThreadGroup操作的坑
Thread与ThreadGroup关系
在上一篇ThreadGroup介绍中说明了JVM中的每一个线程都会隶属于一个线程组。而且只能属于一个线程组。如何查看当前线程隶属于哪个线程组可以通过代码查看。
System.out.println(Thread.currentThread().getThreadGroup().getName());
新创建的Thread属于哪个ThreadGroup呢?
创建Thread有两种常用方法,下面分别说明两种创建线程的方式和线程组有什么关系。
- 继承Thread类
下面看一下Thread类的构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
- 实现Runnable接口
Runnable接口只有个run方法,所以实现类只需要继承Runnable接口,然后实现run方法即可,示例代码如下:
创建完Runnable接口实现类之后,如果想要创建一个线程最后还得通过构造一个Thread类实现,最后通过调用Thread的start方法启动。private static class RunThread implements Runnable{ @Override public void run() { System.out.println("我是runThread"); } } Thread thread = new Thread(new RunThread()); thread.start();
所以通过实现Runnable接口创建线程的方式,本质还是调用了Thread的构造方法。所以统一分析Thread类的构造方法就可以了解新创建的线程和ThreadGroup的关系。每个Thread类的构造方法底层都是通过调用init方法进行初始化线程的,所以只需要查看init方法实现即可。直接看源码实现:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
... 省略部分代码
}
通过init初始化方法可以看到,除了线程名字不能为空,其他的构造参数都可以为空,在代码中可以看到ThreadGroup参数的处理逻辑。如果ThreadGroup参数为空,字段优先判断是否启用了安全管理器,如果没有启用,则设置ThreadGroup参数为当前运行线程的线程组。
含义就是:如果是main线程组中的线程执行的创建线程逻辑,则新创建的线程也同样隶属于这个main线程组,如果是一个自定义线程组的线程执行的创建线程逻辑,则新创建的线程则隶属于这个自定义线程组。
通过ThreadGroup管理线程组内线程的坑
通过了解Thread和ThreadGroup的关系,我们可以很方便的通过ThreadGroup的api来管理整个分组下的所有线程。但是有些操作却很危险,比如调用ThreadGroup.interrupt()可以中断线程组内所有的线程。但是整个线程组内有多少线程你知道么?虽然我们可以自定义一个ThreadGroup,然后自己创建线程然后显示的与自定义ThreadGroup进行手动关联,但是我们自己创建的线程执行的代码逻辑中也有手动创建线程的逻辑,则意味着代码逻辑中新创建的线程也被关联到了这个自定义ThreadGroup中,如果这个代码逻辑创建的线程属于后台守护线程,则很有可能因为对整个ThreadGroup进行interrupt操作而中断,导致系统出现异常问题。
所以在创建后台守护线程时,应该注意:
- 创建Thread时,最好明确指定一个线程组参数,从而确保自己的线程不会因为其他不可控的线程组操作而影响自己的线程状态。
- 创建后台守护线程时,能够确保执行创建Thread逻辑时能够由主线程先执行,不会被其他线程组内的线程先创建。 从而确保守护线程能够隶属于main线程组。
ThreadGroup引起守护线程中断的真实案例
场景描述:
MQ消费应用使用的druid连接池进行数据库操作,启动后一段时间(几小时到一天),出现获取不到数据库连接问题。druid连接池配置的是延迟加载,MQ消费客户端使用了线程组来进行线程管理,当线程组内的线程开始消费mq时,会触发druid连接池创建连接,并且初始化创建了两个守护线程:CreateConnectionThread、DestroyConnectionThread。也就意味着这两个线程隶属于MQ消费客户端内自定义的线程组,这两个守护线程的状态管理统一交给了MQ消费客户端的线程组进行了统一管理。所以当MQ在发生Failover时,收到REMOVE_BROKER事件,会关闭消费线程,并且会将自定义线程组的所有线程关闭(ThreadGroup.interrupt)。而druid连接池的两个守护线程也隶属于这个自定义线程组,所以也被中断了。这就导致了druid连接池中的连接处于了无人守护的地步,如果出现问题,也不再有线程进行检测和重连操作,从而导致druid连接池不可用。
所以针对以上场景的解决方案:
- 避免druid懒加载,在spring中配置init-method="init",应用启动时直接初始化连接池,这样也就确保守护线程隶属于main线程组。
网友评论