ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
每个Thread的内部保存了一个类型为ThreadLocalMap的变量threadLocals。ThreadLocalMap是一个在ThreadLocal中实现的hashmap,Key是ThreadLocal,Value是保存的对象。
get()
获取当前线程对应的值
1 | public T get() { |
get
方法的执行步骤:
- 获得当前线程,取得当前线程中保存的ThreadLocalMap
- 调用
map.getEntry(this)
从ThreadLocalMap中取得Entry - 如果Entry不为null,则从entry中获得value
- 如果Entry为null,则调用
setInitialValue
设置初始值
setInitialValue()
设置初始值
1 | private T setInitialValue() { |
setInitialValue()
方法的执行步骤:
- 调用
initialValue()
方法获取初始值。该方法为public方法,且默认返回null。所以典型用法中常常重载该方法。 - 获取当前线程中保存的ThreadLocalMap
- 如果线程中ThreadLocalMap不为null,在ThreadLocalMap中设置初始值;否则根据初始值在当前线程中创建ThreadLocalMap
这里并不需要考虑ThreadLocalMap的线程安全问题。因为每个线程有且只有一个ThreadLocalMap对象,并且只有该线程自己可以访问它,其他线程不会访问该ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。
set(T value)
根据给定的值设置值
1 | public void set(T value) { |
set
的执行步骤:
- 获得当前线程,取得当前线程中保存的ThreadLocalMap
- 如果ThreadLocalMap不为null,调用set设置value
- 如果ThreadLocalMap为null,调用createMap创建ThreadLocalMap
remove()
删除这个ThreadLocal中当前线程对应的值
1 | public void remove() { |
remove方法的执行步骤
- 首先获取当前线程的ThreadLocalMap
- 然后调用ThreadLocalMap的remove方法将当前ThreadLocal对应的数据删除
ThreadLocal内存泄露问题
当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap里面就会存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。如果当前线程一直存在而没有调用ThreadLocal的remove方法,并且这时候其他地方还是有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。
但是考虑如果这个ThreadLocal变量没有了其他强依赖,而当前线程还存在的情况下,由于线程的ThreadLocalMap里面的key是弱依赖,则当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会被在gc的时候回收,但是对应value还是会造成内存泄露,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项。在ThreadLocal的set
、get
和remove
方法里面有一些时机会对这些key为null的entry进行清理,但是这些清理不是必须发生的。
ThreadLocalMap内部Entry中key使用的是对ThreadLocal对象的弱引用,这为避免内存泄露是一个进步,因为如果是强引用,那么即使其他地方没有对ThreadLocal对象的引用,ThreadLocalMap中的ThreadLocal对象还是不会被回收,而如果是弱引用则这时候ThreadLocal引用是会被回收掉的,虽然对于value还是不能被回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项,虽然ThreadLocalMap提供了set
、get
和remove
方法在一些时机下会对这些Entry进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用remove方法才是解决内存泄露的王道。
ThreadLocalMap的关键方法
getEntry(ThreadLocal<?> key)
1 | private Entry getEntry(ThreadLocal<?> key) { |
- 获取key的hashcode
- 从table中获取Entry
- 如果entry不为null且entry保存的key和当前的key相等,返回这个entry
- 如果在直接hash到的位置中没有找到Entry,则调用
getEntryAfterMiss(key, i, e)
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
getEntryAfterMiss是在hash计算得到的位置中找不到数据时,寻找其他冲突位置的方法。
1 | private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
主要的流程就是循环获取下一个Entry不为null的位置的数据:
- 如果这个位置的key和给定的key相等,则返回这个位置的Entry
- 如果这个位置的key为null,则调用
expungeStaleEntry
方法将这个位置中失效的数据清除
expungeStaleEntry(int staleSlot)
expungeStaleEntry方法擦除失效位置的数据,然后对从这个位置开始直到下一个null位置中的数据重新hash,如果在这个过程中遇到了失效的数据也会将这些失效数据清除。
1 | private int expungeStaleEntry(int staleSlot) { |
set(ThreadLocal<?> key, Object value)
set方法将key与value的组合设置到ThreadLocalMap中
1 | private void set(ThreadLocal<?> key, Object value) { |
set方法的主要步骤为:
- 首先计算key的hashcode,获取这个key的初始位置
- 循环遍历这个key的初始位置到下一个null的位置
- 如果某个位置的key与给定的key相等,将value设置到这个位置的Entry中
- 如果这个位置的key为null,调用
replaceStaleEntry
方法将这个失效的位置替换给定的key和value 如果遍历结束之后,仍然没有找到合适的位置:
- 将我们的key和value设置在这个null的位置上(位置为i)
- 调用
cleanSomeSlots
方法,遍历这个table,如果有失效的Entry被删除则返回true
,否则返回false
- 如果
cleanSomeSlots
方法返回false
,且当前table中存储的数据大于threshold,调用rehash
方法,重新对table进行hash
replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)
replaceStaleEntry
方法的功能是将失效位置的数据替换为我们给定的key和value
1 | private void replaceStaleEntry(ThreadLocal<?> key, Object value, |
cleanSomeSlots()
cleanSomeSlots
的目的是扫描并删除空闲的位置
1 | private boolean cleanSomeSlots(int i, int n) { |
rehash()
1 | private void rehash() { |
首先调用expungeStaleEntries
方法擦除失效的数据,如果table中存储的数据个数大于等于(threshold - threshold / 4)
,调用resize
方法扩容
expungeStaleEntries()
1 | private void expungeStaleEntries() { |
expungeStaleEntries
方法遍历table的每个位置,如果发现这个位置的数据失效,则调用expungeStaleEntry
方法清除这个位置的数据
resize()
resize
方法的功能就是将table的容量扩大为原先的两倍,然后将原先的数据重新hash。
1 | private void resize() { |
remove(ThreadLocal<?> key)
1 | private void remove(ThreadLocal<?> key) { |
remove方法的主要步骤:
- 遍历table直到遇见一个null的entry
- 如果找到了这个Entry,将其删除,然后擦除这个位置的Entry
总结
ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。优势:
- 这样设计之后每个Map的Entry数量变少了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能
- 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/