述
上文中了解了 ThreadLocal
的常用场景和最基本的用法之后再来看一下它的原理以及一些要注意的事项
Thread,ThreadLocal,ThreadLocalMap
首先需要了解一下 Thread
,ThreadLocal
,ThreadLocalMap
这三个类之间的关系,如图
就是一个线程 (Thread) 对应有一个 ThreadLocalMap
, ThreadLocalMap
中存放这一堆的 ThreadLocal
我们可以看一下具体对应的代码
Thread类:
然后再点进去 ThreadLocalMap
类里
可以看到 ThreadLocalMap
这里存储的是一个键值对 Entry
类型的
重要方法解析
第一个 ThreadLocal.setInitialValue()
,设置初始化值的方法
首先看一下源码
总体来说这个方法就是给 ThreadLocal
去设置值的, 这里首先会调用 initialValue
这个方法,这个是不是很熟悉, 就是我们上文中的案例中初始化 ThreadLocal
时,重写的方法,默认是返回空的
setInitialValue()
这个方法会在 get()
方法中调用,再来看一下 get()
方法的源码
先去Map中找,没有的话再去初始化,从这里也可以看出,我们设置的初始化值是懒加载的
还有两个常用的方法就是 set()
和 remove()
这两个这里就不贴代码了
注意事项
内存泄露
使用 ThreadLocal
应该注意内存泄露的风险
内存泄露是指某个对象已经没用了, 但是占用的内存不能被会GC回收
为什么会出现内存泄露我们来看一下
这里看一下存放键值对的这个 Entry
,它是继承了一个 WeakReference
弱引用类型的,用来存放key
弱引用的特点是如果这个对象只被弱引用关联,那么这个对象就会被回收,所以这里的key是可以被回收掉的,但是 value 是个强引用,它不会被回收掉
所以 ThreadLocalMap
的每个 Entry
都包含了一个key的弱引用,和一个value的强引用, 正常情况下,当线程终止保存在 ThreadLocal
中的 value 就会被回收, 但是如果一个线程要持续很久,那它的value就不会被回收掉,即使这个value已经没用了
所以因为 value 和 Thread
之间存在强引用,就可能导致内存泄露
解决方案
那么针对这种情况,我们应该如何去解决呢?
其实jdk已经帮我们做了一些处理, 在调用 set()
remove()
rehash()
这些方法的时候,系统会扫描所有的key为null的 Entry
,把这些对应的 value 都置空,这样value对象就可以被回收,具体是调用的 resize()
这个方法,代码如下
虽然jdk帮我们处理掉一些情况,但是如果 ThreadLocal
用不到了,那基本也不会去调用这几个方法, resize()
也就不会被触发,那么value还是不能被回收的
这种情况下,我们只好在使用完毕之后手动去调用 remove()
方法,避免内存泄露,这也是阿里巴巴开发规范中所规定的
共享对象
第二个要注意的就是 ThreadLocal
中不能存放共享变量,如果放进去的是一个共享变量,比如用 static 修饰的对象,那么 ThreadLocal.get()
获取到的还是这个对象本身,就还是会有线程安全的问题
总结
- 了解
Thread
,ThreadLocal
,ThreadLocalMap
之间的关系
了解常用的几个方法
setInitialValue()
get()
set()
remove()
最后还有使用 ThreadLocal
造成的内存泄漏的原因和解决方法