简单使用
kotlin
复制代码
class ThreadLocalTest { companion object { /** * 定义一个ThreadLocal变量 */ private val threadLocal = object : ThreadLocal<Int>() { /** * 重写该方法,指定一个初始值 */ override fun initialValue(): Int { return 1 } } @JvmStatic fun main(args: Array<String>) { /** * 开启子线程 */ thread(name = "threadLocal-1") { // 先获取旧值 val oldValue = threadLocal.get() // 设置新值 threadLocal.set(3) // 再获取新值 val newValue = threadLocal.get() // 打印线程名称,旧值,新值 println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue") } /** * 开启子线程 */ thread(name = "threadLocal-2") { // 先获取旧值 val oldValue = threadLocal.get() // 设置新值 threadLocal.set(8) // 再获取新值 val newValue = threadLocal.get() // 打印线程名称,旧值,新值 println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue") } } } }
运行结果
实现原理
类图
从图中我们可以看到,在我们的测试类
ThreadLocalTest
中,开启了两个线程,thread-1
和thread-2
,分别在两个线程中调用我们创建好的threadLocal
变量的set
和get
方法,先分别看下set
和get
的源码
set方法
java
复制代码
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap成员变量 ThreadLocalMap map = getMap(t); if (map != null) // 如果能获取到,则直接设置值 map.set(this, value); else // 否则创建ThreadLocalMap对象再赋值 createMap(t, value); }
get方法
java
复制代码
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap成员变量 ThreadLocalMap map = getMap(t); if (map != null) { // 获取map对象中对应的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 取出key对应的值并返回 T result = (T)e.value; return result; } } // 否则创建ThreadLocalMap对象并返回initialValue()方法的值 return setInitialValue(); }
getMap()
java
复制代码
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
createMap()
java
复制代码
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
从
set
方法的源码中我们可以看到,首先会获取到调用set
方法时程序所在的线程,然后再拿到当前线程对应的ThreadLocalMap
成员变量,如果不为空,再调用ThreadLocalMap
成员变量对应的set
方法,把当前ThreadLocal
对象作为key
,保存到ThreadLocalMap
对象中,为空,则创建再保存相应的值,ThreadLocalMap
对象会持有一个Entry
类型的数组,Entry
是一个类似于Map
结构的类,继承于WeakReference
,get
方法也有类似的逻辑,不做详细分析。
结论
不难发现,我们在调用
ThreadLocal
的set
或者get
方法时,内部实际上调用的是ThreadLocalMap
的set
或者get
方法,我们可以理解为ThreadLocal
只是对set
或者get
方法做了一层封装,而ThreadLocal
本身作为一个壳供外部使用,而ThreadLocalMap
是线程持有的成员变量,因此,我们在多线程的场景中使用ThreadLocal
来保存变量,实际上就是不同的线程的成员变量ThreadLocalMap
对变量做了一个副本,对变量的值的改变也只是改变了当前线程的变量的值,不会影响到其他线程的变量的值的改变,起到了一个线程隔离的作用。
关系图
简单解释下,
ThreadLocalMap
是Thread
中的成员变量,ThreadLocalMap
中持有Entry
对象,Entry
是一个类似于Map
的数据结构的类,ThreadLocal
作为key
,Object
作为值被保存下来,Entry
是一个数组,目的是为了在同一个线程中能够保存多种数据类型的变量副本。
内存泄漏问题
为什么会导致内存泄漏
ThreadLocalMap
使用ThreadLocal的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统GC
的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread
->ThreaLocalMap
->Entry
->value
,永远无法回收,造成内存泄漏。 其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。 但是这些被动的预防措施并不能保证不会内存泄漏
为什么ThreadLocal设计成弱引用
在源码当中,
ThreadLocal
被设计成了弱引用,目的是降低OOM
的可能性,不会因此避免OOM
,我们都知道,弱引用在JVM
中,当系统进行一次GC
时,会回收掉被标记为弱引用的对象,ThreadLocalMap
的生命周期是跟Thread
的生命周期一致的,如果Thread
的生命周期足够长,Thread
会一直持有ThreadLocalMap
对象:
- 如果是强引用,
ThreadLocalMap
也会一直持有ThreadLocal
,作为key
的ThreadLocal
也就会随着ThreadLocalMap
的生命周期一直存在,得不到释放,慢慢累积,key
会越积越多,最终导致内存泄漏。- 如果是弱引用,
GC
时,当对ThreadLocal
的强引用被回收时,系统会自动回收掉这部分ThreadLocal
,并且当我们在下一次调用set
、get
或者remove
方法时,也会去清除掉这部分ThreadLcoal
,从而降低对内存的消耗,但是ThreadLocalMap
依然还持有对Entry
的引用,而Entry
对value
也是强引用,因此如果不手动释放,也会造成内存泄漏
如何避免
既然知道了造成内存泄漏的原因,那我们也就能够对症下药了,就是在每次使用完
ThreadLocal
的时候调用remove()
方法清除掉value
就行了。