由于错误使用TransmittableThreadLocal导致的线程污染问题

前提

使用阿里巴巴的transmittable-thread-local可以让外部线程的ThreadLocal变量副本在线程池中也可以访问到,并且在请求结束后会把线程池中的属于外部线程的ThreadLocal变量清除掉,恢复原本的线程池中的线程ThreadLocal信息。

Transmittable-thread-local有两种使用方式

首先引入jar包

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

方案一:使用TransmittableThreadLocal并且需要使用TtlRunnable和TtlCallable修饰传入线程池的Runnable和Callable

官网示例

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置

context.set("value-set-in-parent");
Callable call = new CallableTask();
// 额外的处理,生成修饰了的对象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);

// =====================================================

// Call中可以读取,值是"value-set-in-parent"
String value = context.get();

方案二:使用TransmittableThreadLocal并且使用工具类TtlExecutors修饰线程池,避免每次都要修饰Runnale或者Callable

官网示例:

ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);


TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================

// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();

问题

我们这里的场景是使用了TransmittableThreadLocal,但是并没有使用TtlRunnable和TtlCallable修饰传入线程池的Runnable和Callable,也没有使用工具类TtlExecutors修饰线程池,这样导致外部线程的ThreadLocal变量永远留在了线程池里面,这样在线程池里根据ThreadLocal信息取数据的时候取到了错误的数据。

看一下TransmittableThreadLocal

public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {

它继承自InheritableThreadLocal,而InheritableThreadLocal是会在创建子线程的时候继承父线程的InheritableThreadLocal变量的,具体可以看

java.util.concurrent.Executors.DefaultThreadFactory#newThread

线程初始化时有这么一段代码

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {

//…

  Thread parent = currentThread();
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//…
}

parent是外部线程

inheritThreadLocals默认是true

因此,创建的子线程会留有父线程的InheritableThreadLocal变量副本,并且一般情况下不会被清除。

在我们的项目里问题就是比如线程池中初始线程数为0,然后A请求进来新建了一个线程,并且设置TransmittableThreadLocal值为1,而B请求进来不需要再创建线程,因为之前A请求创建好的线程池线程空闲了,正好被B请求拿来用,那么原本B请求的TransmittableThreadLocal对应值应该为2,但是因为线程是A请求创建的,并且TransmittableThreadLocal已经被A请求设置为1了,所以在线程池线程中会取到错误的TransmittableThreadLocal变量。

因为我们会根据这个变量去数据库取数据,所以取到了错误的数据。

解决方法

解决方案一:按照文档使用工具类TtlExecutors修饰线程池

executorService = TtlExecutors.getTtlExecutorService(executorService);

它会使用ExecutorTtlWrapper包装传入的线程池,而ExecutorTtlWrapper实现了Executor接口,实现了方法execute,会自动帮我们包装Runnable对象。

public void execute(@NonNull Runnable command) {
    executor.execute(TtlRunnable.get(command));
}

TtlRunnable的创建在父线程中,那么在构造的时候它会拿到父线程的所有TransmittableThreadLocal类型的变量,然后执行时会备份子线程的所有ThreadLocal变量,并且将父线程的TransmittableThreadLocal快照设置到当前线程中,当业务逻辑执行完之后,再恢复子线程的ThreadLocal变量。

这是快照父线程的TransmittableThreadLocal,以及用户自己注册的ThreadLocal。

private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    this.capturedRef = new AtomicReference<Object>(capture());
    //...
}

这里是Runnable的run方法。

@Override
public void run() {
  // 这里是拿到快照的值。
  Object captured = capturedRef.get();
  if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
      throw new IllegalStateException("TTL value reference is released after run!");
  }

  //设置到当前线程中,即replay,backup是备份的子线程的ThreadLoal数据
  Object backup = replay(captured);
  try {
      runnable.run();
  } finally {
      // 最后会恢复子线程的ThreadLocal数据
      restore(backup);
  }
}

因此通过组合使用TransmittableThreadLocal和TtlExecutors#getTtlExecutorService可以在线程池线程中拿到调用线程的TransmittableThreadLocal变量,并且在线程池线程执行完毕后清除掉调用线程的TransmittableThreadLocal变量,从而不会污染线程池线程。

解决方案二

如果不需要向线程池线程传递ThreadLocal变量,那么直接使用普通的ThreadLocal即可,而不是TransmittableThreadLocal,需要传递值就手动传递进去。

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值