TransmittableThreadLocal背景 目前我们公司项目记录日志,有三个必须要记录的信息:site、tra
背景
目前我们公司项目记录日志,有三个必须要记录的信息:site、traceId、以及guid,其中site是和具体业务相关的,用来切换数据源的,traceId就是链路跟踪id通常一个请求一个,guid是跟用户走的。在项目里现在获取这三个信息的方式就是通过入参传递,这就导致了我设计任何方法(需要记录日志的方法)都需要这三个入参,导致我很反感,但是由于是老系统,我也不敢动,动了的话牵扯范围太广了。
由于老系统越来越大,代码量越来越多,所以领导准备以后将所有新业务放到一个新项目里,提供接口,然后让老项目进行调用。很显然,新项目的日志也需要上面三个值。新项目我是不想再用上面那种方式了,所以写了一个过滤器将入参三个信息分别给存储到线程的3个threadlocal里了。
ThreadLocal、InheritableThreadLocal
InheritableThreadLocal 的内存泄漏
本来打算就是用InheritableThreadLocal的,但是领导看见之后和我说这个有内存泄漏的风险,让我去研究一下阿里的一个ttl。
InheritableThreadLocal 为什么会内存泄漏?
我们先了解下 InheritableThreadLocal 的作用以及内存泄漏的原因:可以将父线程的threadlocal传递给子线程。但是当移除父线程的threadlocal,子线程的threadlocal并不会消失。并且通常来讲子线程是放线程池管理的,不会随着父线程的消失而消失。所以子线程的threadlocal就一直存在,但是已经用不到了,这就造成了内存泄漏了。
下面看InheritableThreadlocal内存泄漏的例子:
public class InheritableThreadLocalSolution {
private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
});
TimeUnit.SECONDS.sleep(5);
inheritableUserNameThreadLocal.remove();
// 提交第二个任务,看看子线程是否还会获得 inheritableThreadLocal的值
// 结果:会的
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 父线程以及移除了,但是子线程仍然能获取到
System.out.println("second From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
});
TimeUnit.SECONDS.sleep(10);
executorService.shutdown();
}
}
其实除此之外,我觉得每次往线程池中提交任务的时候都用try-finally里包围起来,在finally里手动进行remove也是可以解决的 InheritableThreadLocal 内存泄漏问题的。
但是,谁家好人每次往线程池提交任务都这样,太麻烦了不是,多出来的约定。
那有没有一种更加优雅的threadlocal呢?
就是当前线程要开启子线程,不需要我手动传递当前线程的threadlocal,子线程自动就获取了。但是在当前线程的threadlocal移除了,那么相关子线程的threadlocal也要移除。
TransmittableThreadLocal
这个就是领导说的ttl了。
关于子线程的threadlocal生命周期和父线程threadlocal生命周期是一致的。
public class TransmittableThreadLocalTest {
private static final TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
transmittableThreadLocal.set("Parent Thread (Inheritable)");
// 注意这里的写法,这是ttl线程池装饰器,必须这样写才生效
ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("From child thread (transmittableThreadLocal): " + transmittableThreadLocal.get());
});
TimeUnit.SECONDS.sleep(5);
transmittableThreadLocal.remove();
// 提交第二个任务,看看子线程是否还会获得 inheritableThreadLocal的值
// 结果:不会
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 父线程以及移除了,子线程同步得不到
System.out.println("second From child thread (transmittableThreadLocal): " + transmittableThreadLocal.get());
});
TimeUnit.SECONDS.sleep(10);
executorService.shutdown();
}
}
具体使用
定义一个日志上下文持有的信息
public class LogContextHolder {
private static final TransmittableThreadLocal<String> traceIdHolder = new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<String> guidHolder = new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<String> siteHolder = new TransmittableThreadLocal<>();
public static void setLogInfo(String traceId, String guid, String site) {
traceIdHolder.set(traceId);
guidHolder.set(guid);
siteHolder.set(site);
}
public static String getTraceId() {
return traceIdHolder.get();
}
public static String getGuid() {
return guidHolder.get();
}
public static String getSite() {
return siteHolder.get();
}
public static void clear() {
traceIdHolder.remove();
guidHolder.remove();
siteHolder.remove();
}
}
在过滤器里进行设置
@Order(1)
public class PhoenixFeignLogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Map<String, String[]> parameterMap = servletRequest.getParameterMap();
String traceId = Objects.nonNull(parameterMap.get("traceId")) ? parameterMap.get("traceId")[0] : null;
String guid = Objects.nonNull(parameterMap.get("guid")) ? parameterMap.get("guid")[0] : null;
String site = Objects.nonNull(parameterMap.get("site")) ? parameterMap.get("site")[0] : null;
LogContextHolder.setLogInfo(traceId, guid, site);
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
LogContextHolder.clear();
}
}
}
有了上面两个类,后面设计任何方法就不需要特意留这三个日志信息参数了,直接去当前线程的threadlocal里去取即可。
TransmittableThreadLocal 缺点
安全 和 性能是相反的一组质量属性,如果要求非常安全,那么性能这个质量属性势必会降低。
同理TransmittableThreadLocal解决了这父子线程的相关问题,那么势必也会导致一些其它问题,例如:
- 复杂性增加
- 性能影响
特别是在频繁创建子线程或进行大量的线程上下文传递的情况下,这种性能开销可能会变得比较明显。
总结
原生ThreadLocal的局限性:创建子线程,threadlocal值不会自动传过去。
InheritableThreadLocal的局限性:创建的子线程,threadlocal会自动传过去,但是threadlocal的生命周期和父线程不一致,并且通常子线程是在线程池中的线程是一直存在的,所以会造成内存泄漏。
TransmittableThreadLocal:创建子线程会将threadlocal传过去,并且子线程的threadlocal和父线程生命周期一致。
转载自:https://juejin.cn/post/7408202391504191503