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

一、ThreadLocal是什么?

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

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

二、 ThreadLocal深入源码剖析

在分析源码之前先画一下ThreadLocal ,ThreadLocalMapThread的关系:

由该图可知,Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量。

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

下面简单分析ThreadLocal的set、get和remove方法实现逻辑。

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呢?

其实这和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个字符