likes
comments
collection
share

TransmittableThreadLocal背景 目前我们公司项目记录日志,有三个必须要记录的信息:site、tra

作者站长头像
站长
· 阅读数 20

背景

目前我们公司项目记录日志,有三个必须要记录的信息: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
评论
请登录