如何理解ThreadLocal
全局容器这个概念相信大家都不陌生了,就是在任何地方都可以访问得到的一个容器
在一个类里面声明static的容器,可用称之为全局容器
arduino
public static final Map<String,String> SERVERS_LIST = new ConcurrentHashMap<>(16);
那ThreadLocal怎么理解呢?
我们同样可以把ThreadLocal理解为一个全局容器,它们有一个共同的特点,允许在任何地方从全局容器拿到东西
不同点在哪?ThreadLocal是独属于当前线程的全局容器,而上面讲的Map容器是属于所有线程的全局容器
即每个线程本身就有一个ThreadLocalMap 容器,最经典的案例可以看一下SpringSecurity 的ContextHolder容器,其本质就是通过ThreadLocal实现的,具体大家可以往下看
ThreadLocalMap的类结构体
每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量
-
每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals)
-
Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
-
Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值
-
对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
这也是为什么ThreadLocal能够做到线程隔离的原因
如果这张图还是不太清楚,可以再看看下面几张图
ThreadLocal内存泄漏 - 为什么set后要remove?
内存泄漏问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄漏堆积将会导致内存溢出。
ThreadLocal的内存泄漏问题一般考虑和Entry对象有关 ,在上面的Entry定义可以看出ThreadLocal::Entry被弱引用所修饰
既然提到强弱引用,难道说ThreadLocal内存泄漏问题就是因为强弱引用导致的吗?其实不然,我们分类来讨论一下
-
使用强引用
使用了强引用会发生什么呢?
当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,
无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留
也就是说,如果使用强引用可能就会出现内存泄漏的问题
-
使用弱引用
当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除 ,这个时候Entry中的KEY=null 。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄漏的问题。
总结与思考
根据上面强弱引用的讨论,我们可以知道使用ThreadLocal造成内存泄漏跟强弱引用没有关系
造成内存泄漏的真正原因是:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄漏
因此,需要我们在每次使用完后手动的remove掉Entry对象,但是我对此有几个疑问并结合了我自己的思考:
-
既然ThreadLocalMap和Thread的生命周期是一样的,那在当前线程结束后,ThreadLocalMap不是也会被销毁吗?
怎么就造成了内存泄漏了呢?线程池中的线程复用有关
当一个线程执行完毕并被正确地回收时,它内部的ThreadLocalMap实例也会随着线程一同被垃圾回收器回收,因此不会出现内存泄漏的问题。然而,在使用ThreadLocal结合线程池的情况下,可能会遇到内存泄漏的风险
在线程池中,工作线程通常会复用,而不是每次任务完成后就销毁。这意味着线程会持续存在,即使某个使用了ThreadLocal的任务已经完成了它的生命周期
-
既然强弱引用都会导致内存泄漏,为什么用的是弱引用而不是强引用呢?
在ThreadLocalMap的set/getEntry 中,会对key进行判断,如果key为null,那么value也会被设置为null ,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!