ThreadLocal:线程中的全局变量 | 京东云技术团队
最近接了一个新需求,业务场景上需要在原有基础上新增 2 个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。
如果全链路都变更入参和结构,很明显代码上很不优雅,后续如果还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。
到此,会想到有两种解决方案:之前用的比较少的 ThreadLocal 或者使用 redis 缓存。考虑到新增字段都是些增删改查的操作,没有必要存到 redis 中,故使用 ThreadLocal。
一、ThreadLocal 定义
以微服务架构为例,服务提供方在收到调用方的请求后,会把这个请求分配给一个线程进行处理。一般来说,一个请求会一直由同一个线程处理,中间不会切换线程,所以如果有一个线程中共享的变量,可以当全局变量使用。
ThreadLocal 实现的就是一个线程中的全局变量,与真正的全局变量的区别在于 ThreadLocal 的变量是每个线程中的全局变量,也就是说不同线程访问到的值是不一样的。其填充的变量属于当前线程,该变量对于其他线程是隔离的。
由定义可以发现,ThreadLocal 有两个特性:每个 Thread 的变量只能由当前 Thread 使用;由于其他线程不可访问,则不存在多线程间共享的问题。
二、修饰
ThreadLocal 提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
ThreadLocal 变量通常被 private static 修饰,这样的好处是当一个线程结束时,它所使用的 ThreadLocal 实例副本都可被回收,避免重复创建。坏处就是这样做可能正好导致内存泄漏。
三、底层实现
ThreadLocal 最朴素的内部实现是 Map<threadlocal, Object>,这是一个 HashMap,又称为 ThreadLocalMap。但 Java 源码并不是 Map<threadlocal, Object>的实现。这是因为如果多个线程访问同一个 map,这个 map 需要是线程安全的,构造比较麻烦。Java 采用了更简单粗暴的做法:每个线程都有自己的 ThreadLocal 专属 map,里面可以存放多个 ThreadLocal 变量,这样就解决了多线程同时操作一个 map 带来的多线程并发问题。
因为要把 ThreadLocal 的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如 DDD 领域模型中写在 Common 模块。
具体的实现如下:
四、致命点
上面提到了的 ThreadLocal 会带来内存泄露的问题,深入分析下:
一个 ThreadLocal 实例对应当前线程的一个对象实例,如果把 ThreadLocal 声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用 static 修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的 ThreadLocal 实例引用。
ThreadLocal 自身并不存储值,而是作为一个 key 来让线程从 ThreadLocal 中获取 value。ThreadLocalMap 中的 key 是弱引用,所以 jvm 在垃圾回收时如果外部没有强引用来引用它,ThreadLocal 必然会被回收。但是,作为 ThreadLocalMap 中的 key,ThreadLocal 被回收后,ThreadLocalMap 就会存在 null,但 value 却不为 null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。
因此,key 弱引用并不是导致内存泄露的原因,而是因为 ThreadLocalMap 的生命周期与当前线程一样长,并且没有手动删除对应的 value。
解决的方法也很简单,只需要打破引用路径中的 ThreadLocalMap 对对象实例的引用即可。也就是在使用完 ThreadLocal 之后,必须调用 ThreadLocal.remove()。
延伸:
为什么要将 Map 中的 key 设置为弱引用呢?
实际上,设置 key 为弱引用能预防大多数内存泄露的情况。如果 key 使用强引用,引用的 ThreadLocal 对象被回收,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,也会导致内存泄露。设置为弱引用后,引用的 ThreadLocal 对象被回收,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被 java GC 回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。
参考文章:
https://www.cnblogs.com/tiancai/p/13141234.html?ivk_sa=1024320u
https://last2win.com/2020/09/05/java-threadlocal/
https://blog.csdn.net/u010445301/article/details/111322569
作者:京东零售 李泽阳
来源:京东云开发者社区 转载请注明来源
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/0950a62ad3e78649d2bc3b958】。文章转载请联系作者。
评论