首页 > 其他分享 >TransmittableThreadLocal 的反复与纠缠

TransmittableThreadLocal 的反复与纠缠

时间:2024-03-06 20:58:46浏览次数:27  
标签:纠缠 get 反复 ForkJoinPool worker 线程 TransmittableThreadLocal null commonPool

TransmittableThreadLocal 相信很多人用过,一个在多线程情况下共享线程上下文的利器
名字好长,以下简称 ttl
本文以两年前一个真实项目开发遇到的问题,从源码的角度分析并解决
环境

item version
java 8
springboot 2.2.2.RELEASE
ttl 2.11.4

代码如下,主线程并行启复数任务丢给线程池处理
点击查看代码
List<ProcDef> defs = createsValidate(procCreate);
List<CompletableFuture<CreateResult>> cfs = Lists.newArrayListWithCapacity(defs.size());
defs.forEach(def -> {
    AbstractTransientVariable variable = procCreate.getProcDefKeyVars().get(def.getProcDefKey());
    CompletableFuture<CreateResult> cf = CompletableFuture.supplyAsync(() -> create(def, variable), threadPoolTaskExecutor)
	    .handle((r, e) -> {
		if (e != null) {
		    expHandle(def.getProcDefKey(), procCreate.getUserId(), variable, e);
		}
		return r;
	    });
    cfs.add(cf);
});

List<CreateResult> results = cfs.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());
if (defs.size() != results.size()) {
    log.error("Process create fail exist: [{}]", JacksonUtil.toJsonString(procCreate));
}

第一行 createsValidate 方法在主线程设置了当前用户的上下文
将用户信息放入了 ttl,方便后续子线程使用
测试的时候, 线程池在处理任务的时候,有时会获取不到主线程 ttl 信息
很奇怪,之前也是一直这样使用,为什么没有问题
于是本地main方法模拟

点击查看代码
UserContext.set(new ContextUser().setUserId("mycx26"));

IntStream.range(0, 10).forEach(e -> {
    Supplier<Void> supplier = () -> {
	String userId = UserContext.get() != null ? UserContext.getUserId() : null;
	System.out.println(Thread.currentThread().getName() + " get: " + userId);
	return null;
    };
    CompletableFuture.supplyAsync(supplier);
});

Thread.currentThread().join();
这里主线程将用户信息放入 ttl,依次将异步任务丢给线程池,任务执行获取 ttl 并打印
点击查看代码
ForkJoinPool.commonPool-worker-9 get: mycx26
ForkJoinPool.commonPool-worker-6 get: mycx26
ForkJoinPool.commonPool-worker-13 get: mycx26
ForkJoinPool.commonPool-worker-4 get: mycx26
ForkJoinPool.commonPool-worker-11 get: mycx26
ForkJoinPool.commonPool-worker-2 get: mycx26
ForkJoinPool.commonPool-worker-6 get: mycx26
ForkJoinPool.commonPool-worker-15 get: mycx26
ForkJoinPool.commonPool-worker-8 get: mycx26
ForkJoinPool.commonPool-worker-9 get: mycx26
从输出结果看,各线程都拿到了用户信息,似乎又没有问题
将相同的代码放到工程的单元测试方法里跑
点击查看代码
ForkJoinPool.commonPool-worker-10 get: null
ForkJoinPool.commonPool-worker-15 get: null
ForkJoinPool.commonPool-worker-9 get: null
ForkJoinPool.commonPool-worker-1 get: null
ForkJoinPool.commonPool-worker-13 get: null
ForkJoinPool.commonPool-worker-8 get: null
ForkJoinPool.commonPool-worker-3 get: null
ForkJoinPool.commonPool-worker-2 get: null
ForkJoinPool.commonPool-worker-11 get: null
ForkJoinPool.commonPool-worker-15 get: null
结果却截然相反,到这里我有点怀疑 ttl 对于多线程支持的泛用性了

找到 ttl 的 github readme 阅读
要保证线程池中传递值,一种方式是修饰 Runnable 和 Callable,Supplier 也有类似的包装器
于是修改代码重新测试,测试通过

虽然问题是解决了,但是原因却无从得知,等于还是绕过了问题
下次遇到 ttl 的问题,不知道原理还是无从下手
找到了一个已经 closed 类似的 issue
https://github.com/alibaba/transmittable-thread-local/issues/138
但还是没有解决我的疑问
没有办法,只能看源码了,问题还是要一个一个解决

一. main方法没有修饰的任务为什么能跨越线程池传递 ttl

1.1 首先看看 ttl 的 set 方法做了什么

点击查看代码
public final void set(T value) {
    if (!disableIgnoreNullValueSemantics && null == value) {
        // may set null to remove value
        remove();
    } else {
        super.set(value);
        addThisToHolder();
    }
}
else 走了父类 ThreadLocal 的 set 方法
点击查看代码
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

先看一下 ttl 的继承体系
ttl 继承 InheritableThreadLocal,InheritableThreadLocal 继承 ThreadLocal
InheritableThreadLocal 可以让子线程访问父线程设置的本地变量
点击查看代码
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

通过重写 getMap 和 createMap 方法将 ThreadLocal 的维护职责
由 threadLocals 转移给了 inheritableThreadLocals
threadLocals 和 inheritableThreadLocals 类型一样
是 ThreadLocal 中的静态内部类 ThreadLocalMap
为了维护线程本地变量定制化的哈希map, 两者由 Thread 持有

回到上文 TheadLocal set方法
首先获取当前线程,入参调用 getMap 方法获取当前线程的 inheritableThreadLocals

  • map不为null
    将 ttl 做为 key,value 作为值,放入当前线程的 inheritableThreadLocals

  • map为null
    将 ttl 和 value 构造一个新的 ThreadLocalMap,初始化当前线程的 inheritableThreadLocals

1.2 接下来看 CompletableFuture 的 supplyAsync 方法
这个方法调用栈很深,如果多线程功力不深,基本看不懂
但这不妨碍排查这个问题
supplyAsync 默认用的 ForkJoinPool 跑任务
那么必然会启一个线程
即必然会调用 Thread 的 init 方法初始化线程

首先将断点加到 CompletableFuture.supplyAsync(supplier); 这行
debug跑起来
然后将断点加到 Thread init 方法的第一行
(防止jvm启动初始化的线程产生干扰,比如 c2 complier thread)

点击查看代码
Thread parent = currentThread();

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

重点是这里,判断父线程的 inheritableThreadLocals 如果不为 null
就把父线程的 inheritableThreadLocals 复制到子线程

1.3 接着看 ttl 的 get 方法

点击查看代码
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
    return value;
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

同样走的 ThreadLocal 的 get 方法
首先获取当前线程,getMap 获取其对应的 inheritableThreadLocals
顺利拿到之前父线程设置的变量
到这里,第一个问题算是解了

二. 同样的代码跑在单元测试,没有修饰的任务为什么不能跨越线程池传递 ttl

这里我有理由怀疑是 spring 容器在拉起的时候,提前用到了 ForkJoinPool 的 commonPool
但是项目依赖众多,如何定位
既然用到了,那么将断点加在 ForkJoinPool 启动线程
然后沿着调用栈帧一直向上找不就行了
将断点加在 ForkJoinPool 的 createWorker方法的第一行
开始找

点击查看代码
/* 检查逻辑删除字段只能有最多一个 */
    Assert.isTrue(fieldList.parallelStream().filter(TableFieldInfo::isLogicDelete).count() < 2L,
        String.format("annotation of @TableLogic can't more than one in class : %s.", clazz.getName()));

果然,熟悉的身影,mybatis plus
spring容器拉起时在初始化 SqlSessionFactory 时
会调用 TableInfoHelper 的 initTableFields 方法初始化表主键和字段
注意这里用的 stream 的并行流 parallel stream,很熟悉了
底层默认用的 ForkJoinPool 的 commonPool
那么在主线程设置的 TTL,线程池中的线程之前已经初始化,当然就拿不到了
好,这是第二个问题

三. 为什么项目中自定义线程池获取不到前面主线程创建的 ttl
通过排查发现,执行操作前,线程池已经被调度执行任务了
线程既然已经池化,那么后续在跑异步任务时就没有父子线程之说了
那么现在只剩最后一个问题

四. 为什么项目中任务加了包装器后又拿到了

点击查看代码
TtlWrappers.wrap(() -> create(def, variable))
没有什么办法,跟进去吧,看看 TtlWrappers 的静态方法 wrap 做了什么 未完待续...

标签:纠缠,get,反复,ForkJoinPool,worker,线程,TransmittableThreadLocal,null,commonPool
From: https://www.cnblogs.com/mycx26/p/18051752

相关文章

  • TransmittableThreadLocal 如何解决在分布式环境下线程池中使用ThreadLocal的问题
    在分布式环境下,线程池中使用ThreadLocal会出现线程安全问题,因为线程池中的线程是可以被多个请求共享的,当多个请求同时访问同一个ThreadLocal变量时,会出现数据互相干扰的问题。为了解决这个问题,Java提供了TransmittableThreadLocal类。TransmittableThreadLocal是ThreadLocal的一......
  • 关于AutoCAD反复弹窗Nonvalid Software Detected的解决办法
    事件起因:客户安装的CAD2020频繁弹窗NonvalidSoftwareDetected,报错内容:YOURACCESSISNOWBLOCKED 解决办法:在文件资源管理器中搜索路径C:\ProgramFiles\Autodesk\AutoCAD2020\Support\NewTabPage\config\ACAD\zh-CN(注意自己安装的版本和位置,我这里是2020版本安......
  • Go语言精进之路读书笔记第38条——尽量优化反复出现的if err != nil
    Go在最初设计时就有意识地选择了使用显式错误结果和显式错误检查38.1两种观点显式的错误处理方式让Go程序员首先考虑失败情况,这将引导Go程序员在编写代码时处理故障,而不是在程序部署并运行在生产环境后再处理。而为反复出现的代码片段iferr!=nil{...}所付出的成本已基本被......
  • [word] word 在输入双引号时一直反复出现右双引号怎么办?
    我们在文档中输入双引号时可能出现这样的现象:在输入右双引号后想接着输入左双引号时,左双引号始终打不出来,而是反复出现右双引号,如下图。 ......
  • 服务器反复自动重启/死机的原因
    服务器需要全年不间断地运行,而且它还承载各种应用程序。很多用户在租用服务器的时候会遇到各类问题,本文写的是服务器自动重启/死机可能会出现的原因及解决办法~1.电源是否接触不良首先,第一步就是检查插头是否插紧,检查电源插座是否正常。大概有四分之一的此类故障其实都是因为这样......
  • useMemo依赖没变,回调还会反复执行?
    经常使用React都知道,有些hook被设计为:依赖项数组+回调的形式,比如:useEffectuseMemo通常来说,当依赖项数组中某些值变化后,回调会重新执行。React的写法十分灵活,那么有没有可能,在依赖项数组不变的情况下,回调依然重新执行?描述下Demo在这个示例中,存在两个文件:App.tsxLazy.......
  • zabbix监控主机异常,反复的报警,恢复
    【!!突发:严重问题】PROBLEM:Zabbixagentxxxxisunreachablefor5minutes【主机】:xxxx:122.16.120.165【告警时间】:2022.08.2615:05:17最后发现是,服务器挂着了。另一个服务器的目录,刚好另一个服务器关机了,df-h,卡主看不了磁盘,导致监控异常,开启关闭的服务器,取消监控异......
  • 《yolov5 如果针对一个模型权重反复增加样本训练》
    如果你已经有了一个YOLOv5的模型权重,要使用新的图像数据进行优化,您可以使用以下方法来获得新的模型权重:1.重新训练模型:将新的图像数据与原有的图像数据一起作为训练数据,以更快的速度重新训练模型。2.增量式学习:在原有的模型权重的基础上,通过训练新的图像数据来进行更......
  • 谈谈对TransmittableThreadLocal的理解
    前言最近遇到一个问题,公司内部有一个公共的SSO包,用来获取HTTP请求中的登录态,代码中会直接用这个包的方法获取用户登录信息,在代码任意位置直接用SSOUtil.getUser()获取用户信息,在我们一个下载的业务代码中,用到了线程池开启子任务处理请求,结果发现子任务中拿到的用户信息和HTTP请......
  • c语言当中的COORD ,GetStdHandle(),SetConsoleCursorPosition(),以及避免清屏和反复刷新
    这是WindowsAPI定义的结构体类型COORD来表示字符在控制台屏幕上的坐标,结构体类型COORD定义为:typedefstruct_COORD{SHORTx;SHORTy;}COORD;使用WindowsAPI GetStdHandle()从一个特定的标准设备获取表示设备的句柄(用来标识不同设备的一个数值),SetConsoleCursor......