对ThreadLocal的理解?ThreadLocal如何解决内存泄漏问题?

一、ThreadLocal是什么?

ThreadLocal是一个本地线程副本变量工具类。ThreadLocal中填充的变量只属于当前线程,与其他线程无关。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

二、 ThreadLocal深入源码剖析

ThreadLocal常用的三个核心方法

public T get()               //用于获取当前线程的副本变量值
public void set(T value)     //用于保存当前线程的副本变量值
public void remove()         //移除当前线程的副本变量值
public void initialValue()   //为当前线程初始副本变量值

咱们这里以set(T value)方法为例来探究一下ThreaLocal,下面是set方法的jdk源码,看上去很常规但是却不普通!

流程

  • 1、获取当前线程
  • 2、尝试获取当前线程的ThreadLocalMap(就是一个类似于HashMap的东西),如果Map是空的,那么就会实例化一个ThreadLocalMap,同时把值加入到Map中(和3相同)
  • 3、会把当前ThreadLocal对象作为key,传入的value作为value,添加到当前线程的ThreadLocalMap中

在上面的过程中为什么总是强调当前线程的ThreadLocalMap呢?我们点击getMap()方法一看就知道了。

拿到ThreadLocalMap程序会首先判断一下当前线程中的这个ThreadLocalMap是否为空,如果为空(一般是在第一次执行的时候)那么会实例化一个ThreadLocalMap给当前线程,下面是jdk源码(是不是非常简单):

当ThreadLocalMap不为空的时候,程序在添加新值就会执行ThreadLocal内部的一个私有set()方法,在下面的程序中操作Entry数组是这个方法流程的核心:

  • 1、首先程序拿到当前线程的当前线程的ThreadLocalMap的Eentry
  • 2、计算出这个Entry数组的长度以及hashCode
  • 3、中间若干过程忽略,最终他以当前ThreadLocal为key,传入的值为value,以Entry的形式添加到了当前线程的ThreadLocalMap中

抱着对真相的探究,我们继续往下追,去看看这个Entry是何方神圣!!!

原来这个Entry是ThreadLocalMap的内部类并且他继承的父类很有意思—WeakReference<ThreadLoccal>,这是一个弱引用,通过前面的分析我们得知,每一对ThreadLocal实例和线程变量副本value都是以一个Entry的形式添加到当前线程的ThreadLoacalMap中的,那它为什么要继承WeakReference<ThreadLoccal>呢?

其实这个ThreadLocal解决内存泄漏有很大的关系,熟悉WeakReference的同学应该都知道,如果一个实例对象只有弱引用的话,那么一旦发生GC无论当前堆空间是否紧张,这个实例是一定会被GC清除的。为了更加清除的说明请看下图,tl是我们在桟空间的一个引用,他和ThreaLocal是强引用,正常使用没啥问题,但是当tl不用的时候,如果Entry中的key如果和ThreadLocal是强引用的话,那么这个ThreadLocal就产生了内存泄漏,但是如果采用了弱引用就不存在这个问题了,弱引用户自动被GC清除,此时ThreadLocal中的key的泄漏问题解决了。

但是Entry对象中的value在GC的时候是不会被回收的,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。为了解决这个问题,ThreadLocal中提供了一个remove()方法,并且我们也应该在ThreadLocal使用完之后主动的调用这个方法,帮助GC可以清除掉整个Entry。下面我们直接看remove方法的核心实现,逻辑很简单,找到目标key之后直接调用clear方法将此Entry和ThreadLocalMap引用设为null,并且重新计算Entry的布局。至此ThreadLocal的内存泄漏问题才算是解决了。

三、ThreadLocalMap剖析

首先明确一点,ThreadLocalMap是ThreadLocal的内部类,并且它并没有实现Map接口,而是独立实现一个只可以存放以ThreadLocal为key的Map。下面是ThreadLocal的成员变量:

1、Hash冲突如何解决的?

ThreadLocal采用了HashMap不同的解决hash冲突的方法——线性探测方,即:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能

留言区

还能输入500个字符