
ThreadLocal的八个关键知识点
前言
大家好,我是捡田螺的小男孩。
无论是工作还是面试中,我们都会跟ThreadLocal
打交道,今天就跟大家聊聊ThreadLocal
的八个关键知识点哈~
- ThreadLocal是什么?为什么要使用ThreadLocal
- 一个ThreadLocal的使用案例
- ThreadLocal的原理
- 为什么不直接用线程id作为ThreadLocalMap的key
- 为什么会导致内存泄漏呢?是因为弱引用吗?
- Key为什么要设计成弱引用呢?强引用不行?
- InheritableThreadLocal保证父子线程间的共享数据
- ThreadLocal的应用场景和使用注意点
1. ThreadLocal是什么?为什么要使用ThreadLocal?
ThreadLocal是什么?
ThreadLocal
,即线程本地变量。如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。
为什么要使用ThreadLocal
并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题。
为了解决线性安全问题,可以用加锁的方式,比如使用synchronized
或者Lock
。但是加锁的方式,可能会导致系统变慢。加锁示意图如下:
还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal
。使用ThreadLocal
类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。
2. 一个ThreadLocal的使用案例
日常开发中,ThreadLocal
经常在日期转换工具类中出现,我们先来看个反例:
我们在多线程环境跑DateUtil
这个工具类:
运行后,发现报错了:
如果在DateUtil
工具类,加上ThreadLocal
,运行则不会有这个问题:
运行结果:
刚刚反例中,为什么会报错呢?这是因为SimpleDateFormat
不是线性安全的,它以共享变量出现时,并发多线程场景下即会报错。
为什么加了ThreadLocal
就不会有问题呢?并发场景下,ThreadLocal
是如何保证的呢?我们接下来看看ThreadLocal
的核心原理。
3. ThreadLocal的原理
3.1 ThreadLocal的内存结构图
为了有个宏观的认识,我们先来看下ThreadLocal
的内存结构图
从内存结构图,我们可以看到:
-
Thread
类中,有个ThreadLocal.ThreadLocalMap
-
ThreadLocalMap
内部维护了Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型对象值。
3.2 关键源码分析
对照着几段关键源码来看,更容易理解一点哈~我们回到Thread
类源码,可以看到成员变量ThreadLocalMap
的初始值是为null
ThreadLocalMap
的关键源码如下:
ThreadLocal
类中的关键set()
方法:
ThreadLocal
类中的关键get()
方法
所以怎么回答ThreadLocal的实现原理?如下,最好是能结合以上结构图一起说明哈~
-
Thread
线程类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,即每个线程都有一个属于自己的ThreadLocalMap
。 -
ThreadLocalMap
内部维护着Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值。 - 并发多线程场景下,每个线程
Thread
,在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而可以实现了线程隔离。
了解完这几个核心方法后,有些小伙伴可能会有疑惑,ThreadLocalMap
为什么要用ThreadLocal
作为key呢?直接用线程Id
不一样嘛?
4. 为什么不直接用线程id作为ThreadLocalMap的key呢?
举个代码例子,如下:
这种场景:一个使用类,有两个共享变量,也就是说用了两个ThreadLocal
成员变量的话。如果用线程id
作为ThreadLocalMap
的key
,怎么区分哪个ThreadLocal
成员变量呢?因此还是需要使用ThreadLocal
作为Key
来使用。每个ThreadLocal
对象,都可以由threadLocalHashCode
属性唯一区分的,每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子)。看下ThreadLocal
代码:
然后我们再来看下一个代码例子:
再对比下这个图,可能就更清晰一点啦:
5. TreadLocal为什么会导致内存泄漏呢?
5.1 弱引用导致的内存泄漏呢?
我们先来看看TreadLocal的引用示意图哈:
关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,当ThreadLocal
变量被手动设置为null
,即一个ThreadLocal
没有外部强引用来引用它,当系统GC时,ThreadLocal
一定会被回收。这样的话,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。
当ThreadLocal变量被手动设置为null
后的引用链图:
实际上,ThreadLocalMap
的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal
的get
,set
,remove
方法,都会清除线程ThreadLocalMap
里所有key
为null
的value
。
源代码中,是有体现的,如ThreadLocalMap
的set
方法:
如ThreadLocal的get
方法:
5.2 key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?
到这里,有些小伙伴可能有疑问,ThreadLocal
的key
既然是弱引用.会不会GC贸然把key
回收掉,进而影响ThreadLocal
的正常使用?
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)
其实不会的,因为有ThreadLocal变量
引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null
,我们可以跑个demo来验证一下:
结论就是,小伙伴放下这个疑惑了,哈哈~
5.3 ThreadLocal内存泄漏的demo
给大家来看下一个内存泄漏的例子,其实就是用线程池,一直往里面放对象
运行结果出现了OOM,tianLuoThreadLocal.remove();
加上后,则不会OOM
。
我们这里没有手动设置tianLuoThreadLocal
变量为null
,但是还是会内存泄漏。因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass
对象的value
值,即使设置tianLuoClass = null;
引用还是存在的。这就好像,你把一个个对象object
放到一个list
列表里,然后再单独把object
设置为null
的道理是一样的,列表的对象还是存在的。
所以内存泄漏就这样发生啦,最后内存是有限的,就抛出了OOM
了。如果我们加上threadLocal.remove();
,则不会内存泄漏。为什么呢?因为threadLocal.remove();
会清除Entry
,源码如下:
有些小伙伴说,既然内存泄漏不一定是因为弱引用,那为什么需要设计为弱引用呢?我们来探讨下:
6. Entry的Key为什么要设计成弱引用呢?
通过源码,我们是可以看到Entry
的Key
是设计为弱引用的(ThreadLocalMap
使用ThreadLocal
的弱引用作为Key
的)。为什么要设计为弱引用呢?
我们先来回忆一下四种引用:
- 强引用:我们平时
new
了一个对象就是强引用,例如 Object obj = new Object();
即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 - 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
我们先来看看官方文档,为什么要设计为弱引用:
我再把ThreadLocal的引用示意图搬过来:
下面我们分情况讨论:
- 如果
Key
使用强引用:当ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。 - 如果
Key
使用弱引用:当ThreadLocal
的对象被回收了,因为ThreadLocalMap
持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value
则在下一次ThreadLocalMap
调用set,get,remove
的时候会被清除。
因此可以发现,使用弱引用作为Entry
的Key
,可以多一层保障:弱引用ThreadLocal
不会轻易内存泄漏,对应的value
在下一次ThreadLocalMap
调用set,get,remove
的时候会被清除。
实际上,我们的内存泄漏的根本原因是,不再被使用的Entry
,没有从线程的ThreadLocalMap
中删除。一般删除不再使用的Entry
有这两种方式:
- 一种就是,使用完
ThreadLocal
,手动调用remove()
,把Entry从ThreadLocalMap
中删除 - 另外一种方式就是:
ThreadLocalMap
的自动清除机制去清除过期Entry
.(ThreadLocalMap
的get(),set()
时都会触发对过期Entry
的清除)
7. InheritableThreadLocal保证父子线程间的共享数据
我们知道ThreadLocal
是线程隔离的,如果我们希望父子线程共享数据,如何做到呢?可以使用InheritableThreadLocal
。先来看看demo
:
可以发现,在子线程中,是可以获取到父线程的 InheritableThreadLocal
类型变量的值,但是不能获取到 ThreadLocal
类型变量的值。
获取不到ThreadLocal
类型的值,很好理解,因为它是线程隔离的嘛。InheritableThreadLocal
是如何做到的呢?原理是什么呢?
在Thread
类中,除了成员变量threadLocals
之外,还有另一个成员变量:inheritableThreadLocals
。它们两类型是一样的:
Thread
类的init
方法中,有一段初始化设置:
可以发现,当parent的inheritableThreadLocals
不为null
时,就会将parent
的inheritableThreadLocals
,赋值给前线程的inheritableThreadLocals
。说白了,就是如果当前线程的inheritableThreadLocals
不为null
,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal
,数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~
8. ThreadLocal的应用场景和使用注意点
ThreadLocal
的很重要一个注意点,就是使用完,要手动调用remove()
。
而ThreadLocal
的应用场景主要有以下这几种:
- 使用日期工具类,当用到
SimpleDateFormat
,使用ThreadLocal保证线性安全 - 全局存储用户信息(用户信息存入
ThreadLocal
,那么当前线程在任何地方需要时,都可以使用) - 保证同一个线程,获取的数据库连接
Connection
是同一个,使用ThreadLocal
来解决线程安全的问题 - 使用
MDC
保存日志信息。
参考与感谢
- 彻底理解ThreadLocal[1]
- ThreadLocal是如何导致内存泄漏的[2]
- 深入分析 ThreadLocal 内存泄漏问题[3]
参考资料
[1]彻底理解ThreadLocal: https://www.cnblogs.com/xzwblog/p/7227509.html
[2]ThreadLocal是如何导致内存泄漏的: https://zhuanlan.zhihu.com/p/346291694
[3]深入分析 ThreadLocal 内存泄漏问题: https://www.jianshu.com/p/1342a879f523
文章转载自公众号:捡田螺的小男孩
