【InheritableThreadLocal】搭配线程池使用存在问题
🏆 问题描述
服务中多接口报空指针异常,以报错邮件中的接口参数调用接口时,接口调用正常;
推断接口空指针邮件错发
(A接口报空指针,邮件使用的接口信息为B接口信息)
🎯异常定位
能接收到报错邮件,肯定是有空指针异常;报错邮件是根据log4j抓取的异常触发。
接口信息来源
接口信息使用过滤器在接口请求时,将接口信息存放在MDC中。
MDC (Mapped Diagnostic Context) 可以看成是一个与当前线程绑定的哈希表;
空指针异常(定位异常点)
报错的接口中都是使用的多线程处理,在几个接口处理异常时都打印具体异常标识,重新发版之后;多接口异常信息都指向同一个处理异常处理点;
⭐️原因分析
异常处理方式
异步调用,异常处理方法
List<PropertyStatisticsAvailableDataDto> availableDataDtoList = new ArrayList<>();
CompletableFuture<Void> availableDataDtoListFuture = CompletableFuture.supplyAsync(() -> {
return getPropertyStatisticsAvailableDataList(bo);
}, executor).thenAccept(result -> {
if (result != null && !result.isEmpty()) {
availableDataDtoList.addAll(result);
}
}).exceptionally(e -> {
logger.error("可售数据报错" + e.getMessage());
return null;
});
可以看到异常处理机制是使用从线程池中拿到的子线程进行异常处理。那就看一下为什么子线程中存放在MDC 中的接口信息为什么有误?
MDC中接口信息(源码调用)
查看调用源码MDC底层使用InheritableThreadLocal
存储信息的。如下是源码调用。
-
org.slf4j.MDC#put使用
mdcAdapter
put方法
-
mdcAdapter
创建使用org.slf4j.MDC#bwCompatibleGetMDCAdapterFromBinder方法。
-
调用org.slf4j.impl.StaticMDCBinder#getMDCA 创建
Log4jMDCAdapter
对象 -
调用org.apache.logging.slf4j.Log4jMDCAdapter#put
-
ThreadContext
中调用初始化,useStack
,useMap
默认为true -
org.apache.logging.log4j.spi.ThreadContextMapFactory#createThreadContextMap,第一次创建
createDefaultThreadContextMap
方法
-
org.apache.logging.log4j.spi.ThreadContextMapFactory#createDefaultThreadContextMap
8.三个底层方法都使用了InheritableThreadLocal
类;org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap#CopyOnWriteSortedArrayThreadContextMap
InheritableThreadLocal源码分析
查看InheritableThreadLocal
源码,线程init方法中将父线程变量存储到子线程中。如下是源码调用。
InheritableThreadLocal
用来传递父线程生成的变量到子线程中进行使用
,继承了ThreadLocal
类;
Thread
类中维护了ThreadLocal
,InheritableThreadLocal
,数据类型都是ThreadLocalMap(线程私有);
- java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long)初始化对
InheritableThreadLocal
进行处理
⭐️猜想验证
接口信息是存储到InheritableThreadLocal
中的,在线程初始化的时候,会将父线程的变量值赋值给子线程;业务中使用线程池调用子线程,如果子线程并不是新创建的,而是已经创建过的线程,这样就不会更新接口信息(接口信息先存储主线程中);
✨模拟错误
- 创建线程池将核心线程数设置为
- 模拟示例
private static InheritableThreadLocal inheritableThreadLocal= new InheritableThreadLocal<>();
@Override
public void TestOne() {
ArrayList<String> supplyDataDtoList = new ArrayList<>();
//模拟写入接口信息
inheritableThreadLocal.set("TestOne");
CompletableFuture.supplyAsync(() -> {
return getPropertyStatisticsSupplyDataList1();
},executor).thenAccept(result -> {
if (result != null && !result.isEmpty()) {
supplyDataDtoList.addAll(result);
}
}).exceptionally(e -> {
logger.error("TestOne" + e.getMessage());
return null;
});
}
@Override
public void TestTwo() {
//模拟写入接口信息
inheritableThreadLocal.set("TestTwo");
ArrayList<String> supplyDataDtoList = new ArrayList<>();
CompletableFuture.supplyAsync(() -> {
return getPropertyStatisticsSupplyDataList();
},executor).thenAccept(result -> {
if (result != null && !result.isEmpty()) {
supplyDataDtoList.addAll(result);
}
}).exceptionally(e -> {
logger.error("TestTwo" + e.getMessage());
return null;
});
}
//异步方法A
private List<String> getPropertyStatisticsSupplyDataList1(){
ArrayList<String> supplyDataDtoList = new ArrayList<>();
//打印inheritableThreadLocal变量
System.out.println("打印异步方法AinheritableThreadLocal变量->"+Thread.currentThread().getName()+inheritableThreadLocal.get());
return supplyDataDtoList;
}
//异步方法B
private List<String> getPropertyStatisticsSupplyDataList(){
ArrayList<String> supplyDataDtoList = new ArrayList<>();
String name=null;
//打印inheritableThreadLocal变量
System.out.println("打印异步方法BinheritableThreadLocal变量->"+Thread.currentThread().getName()+inheritableThreadLocal.get());
System.out.println(name.toString());
return supplyDataDtoList;
}
- 先请求A接口,然后多次并发请求B接口。控制台打印
- 先多次并发请求B接口,然后请求A接口。控制台打印
🎉结论&解决
Log4j记录日志MDC使用InheritableThreadLocal
,而搭配线程池使用时,高并发情况下子线程InheritableThreadLocal
容易出现不更新父线程变量的情况。
- 对于使用线程池异步处理处理数据,使用主线程进行异常处理;(业务使用这种方式,在不改log4j源码包的情况下,使用这种方式,正确抓取异常)
CompletableFuture<List<PropertyStatisticsAvailableDataDto>> availableDataDtoListFuture = CompletableFuture.supplyAsync(() -> {
return getPropertyStatisticsAvailableDataList(bo);
}, executor);
CompletableFuture.allOf(availableDataDtoListFuture).join();
List<PropertyStatisticsAvailableDataDto> propertyStatisticsAvailableDataDtos=null;
//这里使用主线程处理异常
try {
availableDataDtoList = availableDataDtoListFuture.get();
} catch (Exception e) {
logger.error("可售数据报错" + getStackTrace(e));
}
- 解决线程本地变量在线程池之间的传递问题,可以引入阿里提供的技术:TransmittableThreadLocal
- Log4j2 MDC 集成 TTL 源码
<!-- https://mvnrepository.com/artifact/com.alibaba/log4j2-ttl-thread-context-map -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>log4j2-ttl-thread-context-map</artifactId>
<version>1.3.3</version>
</dependency>
TransmittableThreadLocal方案
- 🍪 Maven依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.2.0</version>
</dependency>
- 🍍使用新ThreadLocal
private static TransmittableThreadLocal<String> inheritableThreadLocal= new TransmittableThreadLocal();
- 🍋对线程池进行处理(官方提供3种方式,这里使用对线程池进行修饰)
Executor ttlExecutor = TtlExecutors.getTtlExecutor(threadPool);
- 先请求A接口,然后多次并发请求B接口。控制台打印;打印结果正常。