ThreadLocal概述
通常情况下,在程序中创建的变量是可以被任何一个线程访问并修改的。那么如果想实现每一个线程都有自己的专属的本地变量该如何解决?
JDK原生的java.lang.ThreadLocal
类作用就是在多线程中为每一个线程创建单独的变量副本,相当于线程单独的private static
类型变量。
ThreadLocal的作用和同步机制:
- ThreadLocal是为了保证多线程环境下数据的独立性
- 同步机制是为了保证多线程环境下数据的一致性
ThreadLocal的使用
在《阿里巴巴Java开发手册》中提到了可以使用ThreadLocal
类来在多线程环境下使用SimpleDateFormat
类
public class DateUtils { |
在需要用到DateFormat
对象的地方,调用:
DateUtils.DF.get().format(new Date()); |
ThreadLocal原理
先从Thread
源码入手:
public class Thread implements Runnable { |
可以看出Thread
类源码中存在变量threadLocals
和inheritableThreadLocals
,它们都是ThreadLocal.ThreadLocalMap
(ThreadLocal类定制化的HashMap,和HashMap功能相似)类型的变量。这两个变量的初始值都是null,只有当前线程调用TheadLocal
的get()
方法或者set()
方法时才会去创建这两个变量。事实上,调用ThreadLocal#set()
或者是ThreadLocal#get()
方法本质上都是调用了ThreadLocalMap#get()
、ThreadLocalMap#set()
方法。
ThreadLocal实现线程隔离的功能,主要也是用到了ThreadLocal
类中的变量threadLocals
,它负责存储对当前线程的独有对象。
ThreadLocal#set()
public void set(T value) { |
上述代码可以得出结论:最终的变量存放在当前线程的TheadLocalMap
中,并不是存储在ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。
每个线程Thread
都具备一个ThreadLocalMap
,ThreadLocalMap
中可以存储ThreadLocal: Object
的键值对。
/** |
假设我们在同一个线程中声明了多个ThreadLocal
对象,Thread
内部都是使用仅有的ThreadLocalMap
存放数据的,数据格式是ThreadLocal: Object
的键值对。
ThreadLocal#get()
public T get() { |
ThreadLocal#remove()
public void remove() { |
remove()
方法直接将ThreadLocal对应的值从当前线程Thread的ThreadLocalMap
中删除,这涉及到内存泄露的问题。实际上
ThreadLocalMap 中使用的 key 为 ThreadLocal
的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉,这个问题我们后面再谈。
ThreadLocal内存泄露问题分析
ThreadLocalMap
中使用的Entry键key是ThreadLocal
的强引用,而值value是强引用。
static class Entry extends WeakReference<ThreadLocal<?>> { |
因此,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,但是value不会被清理掉。如此这般,ThreadLocalMap
中就会出现key为null的Entry,如果我们不做任何措施,value永远都无法被GC,这个时候就会造成内存泄露。
ThreadLocalMap
实现的时候就已经考虑到了这种情况,在调用
set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。在开发中,尽量使用完
ThreadLocal
方法后手动调用remove()
方法。
=> ThreadLocal内存泄露的根源:由于ThreadLocalMap的生命周期和Thread一样长,对于重复利用的线程来说,如果没有手动删除对应key就会导致key为null的Entry对象越来越多,从而导致内存泄露。
为什么不将ThreadLocalMap中的Key设置为强引用
如果Key是强引用
如果key设计成强引用且没有手动remove()
,那么key会和value一样伴随线程的整个生命周期。
假设在代码中使用完ThreadLocal
,
ThreadLocal
的引用被回收了,但是因为ThreadLocalMap
的Entry强引用了ThreadLocal
造成ThreadLocal无法被回收。在没有手动删除Entry并且CurrentThread依然运行的前提下,
始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry
,
Entry就不会被回收,包括了ThreadLocal实例和value,导致Entry内存泄漏
=> 如果ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的
Key设计成弱引用的原因
在调用ThreadLocalMap的
set()
、get()
、remove()
方法时,会清理掉 key 为 null
的记录,这就意味着在使用threadLocal时,即使CurrentThread依然运行,忘记手动调用remove()
方法,弱引用比强引用可以多一层保障:弱引用的
ThreadLocal 会被回收,对应的值value会在下一次调用ThreadLocalMap的
set()
、get()
、remove()
方法时被清理。
正确使用ThreadLocal
1)将ThreadLocal
变量使用private static
修饰,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后手动remove()
它,防止内存泄露。
2)每次使用完ThreadLocal
,都调用它的remove()
方法,清除数据。
参考文章
- https://juejin.cn/post/6844903487193481224
- https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html
- https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html