🗒️ThreadLocal里的变量一定是线程独享的吗?
00 分钟
2024-8-3
2024-8-3
type
status
date
summary
slug
tags
category
password
URL
icon

背景

在并发编程的场景下,我们经常会遇到线程不安全的情况。针对这种线程不安全的场景,我们可以加锁、使用线程安全的容器或集合或者是ThreadLocal来解决。加锁或者使用线程安全的容器或集合都是为了保证同一时刻只有一个线程可以访问该资源,而ThreadLocal则是由于ThreadLocal是每个线程独享的内容,所以也不存在线程不安全的情况。
但是在使用ThreadLocal存储变量的时候,我们无法进行异步编程,因为异步是开启了一个新的线程,原有线程中的ThreadLocal数据不会自动同步到新建线程中,我们也可以手动同步ThreadLocal的数据到新线程中,但是这样的话过程太过繁琐。幸好Java本身支持了InheritedThreadLocal,InheritedThreadLocal继承自ThreadLocal,与ThreadLocal的区别是当子线程创建时,会从父线程继承InheritedThreadLocal的值,这样的话,我们在异步编程的时候就能通过ThreadLocal进行变量传递了。

问题

那我们就用InheritedThreadLocal试试变量传递。我们首先定义了一个基于InheritedThreadLocal的变量传递Utils,
然后我们使用只有一个线程的线程池测试下效果。
输出内容如下:
实测过程中,我们发现InheritedThreadLocal确实可以实现跨线程的变量传递。但是,我们还发现一个很严重的问题,我们在第一个线程执行后,在主线程中修改了fruit对应的值为banana后,这个改动在第二个线程中的变量中生效了,理论上来说,InheritedThreadLocal这个只会在新建线程的时候才会继承数据啊,我们使用了线程池,理论上是线程复用的,我们在主线程中的修改在第二个线程中应该不生效啊

排查

那究竟是怎么回事呢?我们看下InheritedThreadLocal这个类的具体实现就能发现根因。
重点是childValue这个方法,看下默认实现,它是直接返回了父线程中的整个对象,也就是说,整个对象是父线程和子线程共享的。如果是可变对象,那么在父线程中的修改也会影响到子线程,反过来也是同理。因此,上述我们在父线程中的修改影响到了子线程。那么我们如何避免这个问题呢?
参考官方介绍,我们可以重写childValue方法,重写后的方法如下(注意,这里是浅拷贝,如果我们是修改Map对象内部对象的一些属性,不同线程之间还是会有干扰,如果想完全隔离,建议使用序列化等方式的深拷贝):
然后我们再运行上面的测试代码,发现结果输出符合预期:

结论

我们在使用ThreadLocal类进行跨线程的变量传递时,一定要注意对象引用的问题,不只是InheritedThreadLocal这个类存在上述问题,阿里开源的TransmittableThreadLocal由于继承自InheritedThreadLocal,也会存在此类问题,因此我们在开发过程中一定要多加注意,深究源码,否则很容易出现错误。
 
参考资料:
  1. 小伙伴同学们写的 TTL实际业务使用场景 与 设计实现解析的文章(写得都很好! )❤️
    Updated Mar 23, 2023
 

有了InheritedThreadLocal,我们已经可以实现变量的跨线程传递了,那么阿里为什么还要开源TransmittableThreadLocal工具呢?那就是我们后续要讲述的内容了
 
上一篇
ubuntu多用户管理(添加、删除用户,给用户配置权限)
下一篇
微软GraphRAG原理介绍(附带部分源码)