ThreadLocal 解析 [转载]
本文转载自:
ThreadLocal
是什么?
ThreadLocal
是一个线程内部数据存储类,通过它可以在指定的线程中存储数据。存储后,只能在指定的线程中获取到存储的数据,对其他线程来说无法获取到数据。
ThreadLocal
的使用场景
日常使用场景不多,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,可以考虑使用ThreadLocal
。 Android
源码的Lopper
、ActivityThread
以及AMS
中都用到了ThreadLocal
。
ThreadLocal
的使用示例
1 |
|
运行结果:
1 |
|
可以看到虽然访问的是同一个ThreadLocal
对象,但是获取到的值却是不一样的。
ThreadLocal
的源码阅读
那么为什么会造成这样的结果呢?这就需要去看看ThreadLocal
的源码实现,这里的源码版本为API28
。主要看它的get
和set
方法。
1 |
|
set
方法中首先获取了当前线程对象,然后通过getMap
方法传入当前线程t
获取到一个ThreadLocalMap
,接下来判断这个map
是否为空,不为空就直接将当前ThreadLocal
作为key
,set
方法中传入要保存的值最为value
,存放到map
中;如果map
为空就调用createMap
方法创建一个map
并同样将当前ThreadLocal
和要保存的值作为key
和value
加入到map
中。
接下先看getMap
方法:
1 |
|
getMap
方法比较简单,就是返回从传入的当前线程对象的成员变量threadLocals
。 接着是createMap
方法:
1 |
|
createMap
方法也很简单就是new
了一个ThreadLocalMap
并赋给当前线程对象t
中的threadLocals
。 原来这个Map
是存放在Thread
类中的。于是进入Thread
类中查看。
Thread.java
第188-190行:
1 |
|
根据这里的注释可以得知,每个线程Thread
中都有一个ThreadLocalMap
类型的threadLocals
成员变量来保存数据,通过ThreadLocal
类来进行维护。这样看来我们每次在不同线程调用ThreadLocal
的set
方法set
的数据是存在不同线程的ThreadLocalMap
中的,就像注释说的ThreadLocal
只是起了个维护ThreadLocalMap
的功能。想到是get
方法同样也是到不同线程的ThreadLocalMap
去取数据。
get
方法:
1 |
|
果然,get
方法中同样是先获取当前线程对象,然后在拿着这个对象t
去获取到t
中的ThreadLocalMap
,只要map
不等于null
就调用map.getEntry(this)
方法来获取数据,因为ThreadLocalMap
里使用一个内部类Entry
来存储数据的,所以调用getEntry(this)
方法,传入的key
是当前的ThreadLocal
。这样获取到Entry
类型数据e
,只要e
不为null
,返回e.value
即先前存储的数据。如果获取到的map
为null
又或者根据key
获取Entry
为null
,就调用setInitialValue
方法初始化一个value
返回。
setInitialValue
和initialValue
方法:
1 |
|
setInitialValue
方法中首先调用initialValue
方法初始化了一个空value
,之后的操作和set
方法相同,将这个空的value
加入到当前线程的ThreadLocalMap
中去,ThreadLocalMap
为空就创建个Map
,最后返回这个空值。
至此,ThreadLocal
的get
、set
方法就都看过了,也理解了ThreadLocal
可以在多个线程中操作而互不干扰的原因。但是ThreadLocal
还有一个要注意的地方就是ThreadLocal
使用不当会造成内存泄漏。
ThreadLocal
内存泄漏的原因
内存泄漏的根本原因是当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,导致本该被回收的对象不能被回收而停留在堆内存中。那么ThreadLocal
中是在哪里发生的呢?这就要看到ThreadLocalMap
中存储数据的内部类Entry
。
1 |
|
可以看到这个Entry
类,这里的key
是使用了个弱引用,所以因为使用弱引用这里的key
,ThreadLocal
会在JVM
下次GC
回收时候被回收,而造成了个key
为null
的情况,而外部ThreadLocalMap
是没办法通过null
key
来找到对应value
的。如果当前线程一直在运行,那么线程中的ThreadLocalMap
也就一直存在,而map
中却存在key
已经被回收为null
对应的Entry
和value
却一直存在不会被回收,造成内存的泄漏。
不过,这一点设计者也考虑到了,在get()
、set()
、remove()
方法调用的时候会清除掉线程ThreadLocalMap
中所有Entry
中Key
为null
的Value
,并将整个Entry
设置为null
,这样在下次回收时就能将Entry
和value
回收。
这样看上去好像是因为key
使用了弱引用才导致的内存泄漏,为了解决还特意添加了清除null key
的功能,那么是不是不用弱引用就可以了呢?
很显然不是这样的。设计者使用弱引用是由原因的。
- 如果使用强引用,那么如果在运行的线程中
ThreadLocal
对象已经被回收了但是ThreadLocalMap
还持有ThreadLocal
的强引用,若是没有手动删除,ThreadLocal
不会被回收,同样导致内存泄漏。 - 如果使用弱引用
ThreadLocal
的对象被回收了,因为ThreadLocalMap
持有的是ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。nullkey
的value
在下一次ThreadLocalMap
调用set
、get
、remove
的时候会被清除。
所以,由于ThreadLocalMap
和线程Thread
的生命周期一样长,如果没有手动删除Map
的中的key
,无论使用强引用还是弱引用实际上都会出现内存泄漏,但是使用弱引用可以多一层保护,null key
在下一次ThreadLocalMap
调用set
、get
、remove
的时候就会被清除。 因此,ThreadLocal
的内存内泄漏的真正原因并不能说是因为ThreadLocalMap的key
使用了弱引用,而是因为ThreadLocalMap
和线程Thread
的生命周期一样长,没有手动删除Map
的中的key
才会导致内存泄漏。所以解决ThreadLocal
的内存泄漏问题就要每次使用完ThreadLocal
,都要记得调用它的remove()
方法来清除。
总结一波:
(1)每个Thread维护着一个ThreadLocalMap
的引用
(2)ThreadLocalMap
是ThreadLocal
的内部类,用Entry来进行存储
(3)ThreadLocal
创建的副本是存储在自己的threadLocals
中的,也就是自己的ThreadLocalMap
。
(4)ThreadLocalMap
的键值为ThreadLocal
对象,而且可以有多个threadLocal
变量,因此保存在map
中
(5)在进行get
之前,必须先set
,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue
()方法。
(6)ThreadLocal
本身并不存储值,它只是作为一个key
来让线程从ThreadLocalMap
获取value
。
OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal
来说关键就是内部的ThreadLocalMap
。