ThreadLocal在一定程度上保证了线程安全🔐(原子性),他不让多线程去操作临界资源,让每个线程去操作属于自己的数据
ThreadLocal的实现原理
-
每个ThreadLocal都存储着一个成员变量ThreadLocalMap
-
ThreadLocal本身不存储数据,类似一个工具类,我们基于ThreadLocal操作ThreadLocalMap
-
ThreadLocalMap本身是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来就可能需要存储多个数据,所以采用数组实现
-
每一个ThreadLocalMap都是基于当前的"this"(即ThreadLocal)作为key来获取value的
-
ThreadLocalMap的key是一个弱引用(一旦GC就会回收♻️)
- 这里是为了在ThreadLocal对象失去引用后,如果key是强引用,会导致ThreadLocal对象无法被回收
这里我们补充介绍下Java的引用类型
Java的四种引用类型
引用的强软弱虚(引用的强弱关系依次递减)
- 强引用(=) :关系存在,即不会被垃圾回收机制回收(内存溢出抛异常也不会回收)
- 软引用(SoftReference) :内存不够会被回收(回收后仍然OOM才会抛异常)
- 弱引用(WeakReference) :无论内存够不够,垃圾收集器一工作就会被回收,可以解决内存泄漏的相关问题
- 虚引用(PhantomReference) :约等于没有引用关系(甚至无法通过虚引用获取到对象实例),一般不单独使用,需要和引用队列联合使用。主要作用是跟踪对象被垃圾回收的状态,只是会在被回收后发送一条系统通知
ThreadLocal的内存泄漏问题
首先,什么是内存泄漏? 内存泄漏(Java中)指的是在程序运行过程中,被分配到堆中的某些对象,不会再被使用,但却无法被垃圾回收器释放的现象。内存泄漏会导致内存使用量不断增加,可能会导致程序越来越慢甚至崩溃。
-
如果ThreadLocal的引用丢失,key会因为本身是弱引用而被GC回收,如果此时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到
-
为了解决ThreadLocalMap中key的内存泄漏问题,采用了弱引用的方式(下图虚线)
- 弱引用(WeakReference)是无论内存够不够,垃圾收集器一工作就会被回收,因此可以解决key的内存泄漏
-
但是并没有解决上面所说的value的内存泄漏问题
-
我们根据下图来简单分析下原因:
- 图中有ABCDF五个引用,其中ABC为强引用,DF为弱引用;
- 当AB还存在时,ThreadLocalMap、ThreadLocal_1和ThreadLocal_2都能通过引用被获取到,自然不存在内存泄漏的问题;
- 当AB不存在时,则无法通过AB的强引用找到ThreadLocal_1和ThreadLocal_2;
- 由于线程对象C还存在,因此ThreadLocalMap不会被当作垃圾对象被回收;
- 在经过GC后,DF的弱引用会被回收,没有了强引用的ThreadLocal_1和ThreadLocal_2对象也会被回收;
- 此时,则不会再有程序能通过key_2和key_4访问到他们的value(因为ThreadLocalMap是通过ThreadLocal的弱引用作为key的,此时这两个ThreadLocal已经因为没有强引用而被GC回收了),但是由于线程对象还存在,故ThreadLocalMap无法被回收,于是这两个value还将继续存在,这也就造成了内存泄漏
-
解决方法
只需要在使用完ThreadLocal后,已是调用remove()
方法,移除Entry即可
到这里不知道有没有人跟我冒出类似的想法:既然我都记得使用remove()
方法了,直接在这个方法中把key和value移除不就行了,为什么要搞个弱引用呢?个人理解是为了以防万一,由于key指向的是一个ThreadLocal对象,一般会比value大,因此用弱引用来保证较大的ThreadLocal对象会自动被回收,以达到即使忘记使用remove()
方法,仍然能释放较大的对象(即ThreadLocal)的效果(个人理解,欢迎指正👏)