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

一、ThreadLocal是什么?

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

ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题,这种解决多线程的安全问题和lock(这里的lock 指通过synchronized 或者Lock 等实现的锁) 是有本质的区别的:
1. lock 的资源是多个线程共享的,所以访问的时候需要加锁。
2. ThreadLocal 是每个线程都有一个副本,是不需要加锁的。
3. lock 是通过时间换空间的做法。
4. ThreadLocal 是典型的通过空间换时间的做法。

二、 ThreadLocal深入源码剖析

在分析源码之前先画一下ThreadLocal ,ThreadLocalMap 和 **Thread **的关系:

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

set方法
咱们这里以set(T value)方法为例来探究一下ThreaLocal,下面是set方法的jdk源码

流程梳理

  • 1、获取当前线程
  • 2、尝试获取当前线程的ThreadLocalMap(就是一个类似于HashMap的东西),如果map不为空,那就把当前线程和value值绑定并保存到map中,否则如果Map是空的,那么就会先实例化一个ThreadLocalMap,然后把value和当前线程绑定加入到map中(从这里可以看到,ThreadLocal采用了懒加载机制,没有一上来就直接new一个ThreadLocalMap,而是在第一次使用的时候再初始化Map,这一点和HashMap的机制类似。)

在程序第一次执行set方法的时候,ThreadLocalMap还是null,此时就需要初始化一个ThreadLocalMap,下图所示就是初始化一个ThreadLocalMap的过程。

可以看到非常简单的逻辑,就是直接new了一个ThreadLocalMap然后返回。

ThreadLocalMap初始化之后就可以正常的获取map和设置值了,我们点击getMap()方法看看map是如何获得到的,如下图所示。
ThreadLocal.java

可以看到,一个线程之和一个ThreadLocalMap有关联,而且这个还是在Thread类中维护着。

Thread.java

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

上面set方法的源码中多次用到了Entry数组,那么这个Entry到底是啥呢?

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

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

remove方法
虽然弱引用可以解决ThreadLocalMap中key的内存泄漏问题,但是ThreadLocalMap对象中的value在GC的时候是不会被回收的,并且随着GC将key清除之后,value也再无法访问到,如果创建ThreadLocal的线程一直持续运行(比如在线程池中),那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。为了解决这个问题,ThreadLocal中提供了一个**remove()**方法,我们应该在ThreadLocal使用完之后主动的调用这个方法,帮助GC可以清除掉整个Entry。

下面我们直接看remove方法的核心实现,如下图所示。

逻辑很简单,找到目标key之后直接调用clear方法将此Entry和ThreadLocalMap引用设为null,然后调用expungeStaleEntry方法重新计算Entry的布局。至此ThreadLocal的内存泄漏问题才算是解决了。

replaceStaleEntry方法
还记得在set方法中遍历Entry数组时如果遇到key被垃圾回收器回收的情况ThreadLocal是如何处理的吗?没错就调用了replaceStaleEntry方法,这个方法的作用就是当一个key被GC回收之后,那就需要把过期的值替换,设置成为新值。我们来看一下它的实现:

三、ThreadLocalMap剖析

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


* INITIAL_CAPACITY:ThreadLocalMap的初始容量,默认是16
* table:ThreadLocalMap存放数据的地方
* size:Entry数组的大小
* threshold:扩容阈值,默认初始是0,但是在setThreshold方法中设置的值是Entry数组长度的2/3,如下图所示。

1、Hash冲突如何解决的?

ThreadLocal采用了与HashMap不同的解决hash冲突的方法——开放地址法,即:先根据key的hashcode值确定元素在table数组中的位置(这个位置通过key.threadLocalHashCode&(len-1)获得),如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找下一个位置。如果在某个位置上没有元素了,那就把新值放到该位置上。

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

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

2、ThreadLocalMap 采用开放地址法原因

ThreadLocal 中可以看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了
ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

3、ThreadLocalMap 和HashMap比较

ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:

  • HshMap 的数据结构是数组+链表+红黑树(JDK1.8+)
  • TreadLocalMap的数据结构仅仅是数组
  • HshMap 是通过链地址法解决hash 冲突的问题
  • TreadLocalMap 是通过开放地址法来解决hash 冲突的问题
  • HshMap 里面的Entry 内部类的引用都是强引用
  • TeadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用

留言区

还能输入500个字符