背景
线上使用TransmittableThreadLocal解决线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。运行一段时间后,出现内存泄漏,进程被杀。后续回滚。进行分析。
使用知识储备
关于TransmittableThreadLocal的源码分析和使用请参考
问题示例
•我们先看一段使用TransmittableThreadLocal示例代码
/**
* 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
*/
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
@GetMapping("/ttlLeak")
public void ttlLeak() throws InterruptedException {
try{
transmittableThreadLocal.set("val");
Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable1));
}finally {
//删除上下文
transmittableThreadLocal.remove();
Thread.sleep(2000);
System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(runnable2);
}
}
运行后得到如下结果:

通过上图可以看到第一次执行异步任务的时候能够获取到值,之后主线程进行remove操作,主线程获取不到为空,第二个异步任务执行的时候能够获取到值。这种情况下能够说明remove操作对主线程起效果,但是对子线程没有效果。子线程也有父线程也就是主线程的上下文信息。并且remove不掉。子线程是不是存在泄漏呢?那么有人说你没有规范使用TransmittableThreadLocal,接下来看下一段代码
private static TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
/**
* 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
*/
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
@GetMapping("/ttlLeak")
public void ttlLeak() throws InterruptedException {
try{
transmittableThreadLocal.set("val");
Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable1));
}finally {
//删除上下文
transmittableThreadLocal.remove();
Thread.sleep(2000);
System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable2));
}
}
和上一段代码的区别是最后一个异步任务使用了TtlRunnable来包装,我们看下效果如下:

很明显前面输出一致,最后一个没有获取到值了,那么是不是在TtlRunnable.get(runnable2)进行remove了呢?把当前子线程的上下文信息给remove了呢?我们再看一段代码如下:
private static TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
/**
* 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
*/
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
@GetMapping("/ttlLeak")
public void ttlLeak() throws InterruptedException {
try{
transmittableThreadLocal.set("val");
Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable1));
}finally {
//删除上下文
transmittableThreadLocal.remove();
Thread.sleep(2000);
System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable2));
Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(runnable3);
}
}
添加了一段异步任务3的逻辑,我们看下效果如下:

可以看到第三个异步任务仍然可以拿到值,到这个时候我们能够确认TtlRunnable.get(runnable2)
并不能清除上下文信息。会存在泄漏。
泄漏原因定位
这里的最主要是识别TransmittableThreadLocal继承自JDK的InheritableThreadLocal,参考上面的代码当t1.set("val"),执行后相当于主线程InheritableThreadLocal这个线程绑定的值有值了。参考源码如下:


之后第一次ThreadPoolExecutor执行异步任务的时候,会初始化线程,初始化线程的特性会子线程也继承父线程;也就是主线程的InheritableThreadLocal。参考源码如下:

这个时候相当于父子线程都携带了InheritableThreadLocal,后续主线程remove操作,remove的是主线程的InheritableThreadLocal,finally执行第二次异步任务的时候,
获取值为空是因为异步任务用TtlRunnable修饰,用它修饰的话,执行任务前会将用父线程也就是主线程的上下文信息给子线程,这个时候主线程已经被清空,所以展示为空。但是执行完任务后会将子线程原来的上下文信息覆盖给当前子线程。也就是归还回去子线程原来的上下文信息。参考源码如下:

所以最后一个异步任务执行的时候会拿到值 因为上下文信息依旧存在。
线程池的线程是携带InheritableThreadLocal,所以导致能够取到值。
这就是内存泄漏原因,看起来是TransmittableThreadLocal的泄漏其实是JDK的InheritableThreadLocal的泄漏。在父子线程传递的过程中,会让子线程也携带父线程的InheritableThreadLocal的上下文信息。
解决方案
第一种是业务操作前 清空线程池线程上下文;
private static TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
/**
* 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
*/
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100),TtlExecutors.getDefaultDisableInheritableThreadFactory());
@GetMapping("/ttlLeak")
public void ttlLeak() throws InterruptedException {
try{
transmittableThreadLocal.set("val");
Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable1));
}finally {
//删除上下文
transmittableThreadLocal.remove();
Thread.sleep(2000);
System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable2));
Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(runnable3);
}
}
效果如下:

可以看到最后一个异步任务也拿不到值了,说明异步线程的上下文也被清空了。
实现原理参考TransmittableThreadLocal提供了声明线程池初始化前都清空上下文信息,源码如下:

第二种是父子线程继承的时候 子线程初始化为空值。
TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<String>() {
protected String childValue(String parentValue) {
return initialValue();
}
};
/**
* 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
*/
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
@GetMapping("/ttlLeak")
public void ttlLeak() throws InterruptedException {
try{
transmittableThreadLocal.set("val");
Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable1));
}finally {
//删除上下文
transmittableThreadLocal.remove();
Thread.sleep(2000);
System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(TtlRunnable.get(runnable2));
Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
executor.execute(runnable3);
}
}
可以看到最后一个异步任务也拿不到值了,说明异步线程的上下文也被清空了。
原理在于声明了传递过程中子线程初始化的时候值为空,无视parentValue并不继承父线程的值。
与原作者的讨论分析过程
感谢鼎哥的帮助
关于不修饰runnable使用ttl导致子线程泄露问题 · Issue #521 · alibaba/transmittable-thread-local · GitHub
文章通过示例代码展示了在使用TransmittableThreadLocal进行线程间数据传递时可能出现的内存泄漏问题,由于InheritableThreadLocal的特性,子线程会继承父线程的上下文,即使在主线程中remove也无法彻底清除。文章分析了内存泄漏的原因并提出了两种解决方案:一是使用特定的线程工厂禁用InheritableThreadLocal的继承;二是重写TransmittableThreadLocal的childValue方法,使子线程初始化时为空值。
1410

被折叠的 条评论
为什么被折叠?



