一篇ThreadLocal走天下

「尺有所短,寸有所长;不忘初心,方得始终」

「请关注公众号:星河之码」

在面试的时候经常会有人文ThreadLocal是啥,首先明确的一点是:「虽然ThreadLocal提供了一种解决多线程环境下成员变量的问题,但是ThreadLocal与线程同步无关,它解决的也不是多线程共享变量的问题」。那么ThreadLocal到底是什么,又解决了什么问题呢?下面带着以下几个问题来了解了解ThreadLocal

  • ThreadLocal是什么?
  • ThreadLocal用来解决什么问题的?
  • ThreadLocal的实现原理是什么?
  • ThreadLocal是如何实现线程隔离的?
  • ThreadLocal会造成内存泄露如何解决
  • ThreadLocal的应用场景是什么?

一、ThreadLocal是什么

「ThreadLocal叫做线程变量,即ThreadLocal中填充的变量属于当前线程,该变量是当前线程独有的变量,对其他线程而言是隔离的」

一篇ThreadLocal走天下

在ThreadLoal类的注释也有说明:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的正常对应变量,因为访问某个变量(通过其getset方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

  • 「ThreadLocal是一个为每一个线程创建单独的变量副本的类」

    使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。

  • 「ThreadLocal线程隔离,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本」

    每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用,不存在多线程间共享的问题。

「ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,【即变量在线程间隔离而在方法或类间共享的场景】使用」

一篇ThreadLocal走天下

二、ThreadLocal怎么用

ThreadLocal既然是线程变量,并且每个线程都有它的副本,那么它必然是多线程下使用的,下面以一个dome的方式看看ThreadLocal是怎么用的。

public class ThreadLocaDemo {

private static ThreadLocal<Integer> local = new ThreadLocal<Integer>();

public void printThreadVariable(String threadName){
//将当前线程中ThreadLocal中的值打印出来
System.out.println(threadName + "中的线程变量是 :" + local.get());
//将当前线程中ThreadLocal中的值移除
local.remove();
}


public static void main(String[] args) {
ThreadLocaDemo threadLocaDemo = new ThreadLocaDemo();
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.local.set(0);
threadLocaDemo.printThreadVariable(Thread.currentThread().getName());
}
}).start();
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.local.set(1);
threadLocaDemo.printThreadVariable(Thread.currentThread().getName());
}
}).start();
}
}

以上案例中,通过线程池的方式为给共享变量ThreadLocal中set值,不同线程获取的值是自己设置的。「这就说明ThreadLocal的值是线程隔离」

一篇ThreadLocal走天下

这里就有疑问了,

  • 「疑问一:上述案例中每个线程都自己设置了值,自己能拿到不奇怪啊,」

    我们看下面这个案例

    一篇ThreadLocal走天下

    通过这个案例我们发现,第一个子线程设置了值,它拿到了,第二个线程没有设置值,没有拿到,主线程最后执行,也是拿到了自己的值,「再一次证明了ThreadLocal的值是线程隔离」

  • 疑问二:在上述案例printThreadVariable中为什么要调用remove()

    local.remove();

    「remove方法就是移除set的变量」。调用remove方法有两点好处:

    • 「防止内存溢出,当我们的子线程比较多,或者set的值比较大实话,可能会造成内存溢出」

    • 「ThreadLocal是线程隔离的,但是不隔离请求,如果发生线程复用的情况,可能会出现两次请求拿到的变量是一样的」

      线程池就是使用的线程复用技术

三、ThreadLocal原理

翻开ThreadLocal源码,我们发现它最主要的就是四个方法【get、set、remove、initialValue】,一个内部类【ThreadLocalMap】的实现,本质上ThreadLocal只是一个壳子。


「ThreadLocal的主要用途是实现线程间变量的隔离,多个线程表面上共享的是同一个ThreadLocal(比如上述的案例), 但是实际上使用的值value却是线程自己独有的一份,其实现主要就通过ThreadLocalMap」

一篇ThreadLocal走天下

通过这张图来解析一下ThreadLocal,

  • 真正存储变量的是ThreadLocal中的内部类:ThreadLocalMap

  • ThreadLocalMap引用是在Thread上定义的

    每个线程 都有一个自己的ThreadLocalMap,随着线程的消亡而消亡

  • ThreadLocal本身不存储值,它只是提供了一个在当前线程中找到副本值得key,来让线程从ThreadLocalMap获取Value

  • 「ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中」

一篇ThreadLocal走天下

通过以上描述走「总结一下threadlocal 的原理」

「当线程使用threadlocal 时,是将threadlocal当做当前线程thread的属性ThreadLocalMap 中的一个Entry的key值,而Entry的value值(实际要使用的变量值)是实际上存放的变量」

value值为什么不存在并发问题呢?

因为threadlocal做为Key,类似索引的东西,每个线程都有一个threadlocal,不同的threadlocal对应于不同的value值存放在ThreadLocalMap 中,它们之间互不影响,

也就是说:ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,也就不存在并发问题。

上述分析其实主要就围绕两点展开

  • 「ThreadLocal:代表当前线程变量副本」
  • 「ThreadLocalMap:以ThreadLocal为key,存储每个线程的变量副本的值」

接下来我们就来看看这两个类的源码具体实现。

四、 ThreadLocalMap结构分析

我们知道ThreadLocal是通过ThreadLocalMap存储ThreadLocal与变量对应关系实现变量副本的,那么接下来看看ThreadLocalMap 的内部结构时什么样的。

4.1 ThreadLocalMap内部结构

  • 「ThreadLocal与ThreadLocalMap」

    ThreadLocalMap内部结构其Jdk8做了优化,这里是针对Jdk8的分析

    一篇ThreadLocal走天下

    通过上述这张图,我们可以看出:

    • 「每个Thread线程内部都有一个ThreadLocalMap」

    • 「ThreadLocalMap里面存储ThreadLocal对象(key)和线程的变量副本(value)」

      Thread内部的ThreadLocalMap是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值,对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

  • 「多个ThreadLocal」

    当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal;

    在一个线程里面可以有一个或者多个ThreadLocal的,这些ThreadLocal会被存放在ThreadLocalMap中

    ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
    ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>();
    threadLocal1 .set("A");
    threadLocal2 .set(1);

    类似上述这种,在ThreadLocalMap中的模型如下

    一篇ThreadLocal走天下

通过上述分析可以得出一个结论:「ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap的一个工具类,Thread线程可以拥有多个ThreadLocal来维护的自己线程独享的共享变量」

接下来通过源码来看看ThreadLocalMap的几个主要的方法。

4.2 ThreadLocalMap构造方法

先看看ThreadLocalMap对象的创建过程

/**
* The initial capacity -- MUST be a power of two.
*/

private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/

private Entry[] table;


ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// 计算firstKey 的散列值,进行一个位运算得到索引i
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

构造方法中「实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置」

4.3 Entry

ThreadLocal类有个getMap()方法返回Thread对象自身的Map——threadLocals。

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
一篇ThreadLocal走天下

threadLocals是ThreadLocal.ThreadLocalMap类型的数据结构,作为内部类定义在ThreadLocal类中。

虽然ThreadLocalMap是ThreadLocal的内部类,但是其对象由当前线程Thread持有

一篇ThreadLocal走天下

ThreadLocalMap内部利用Entry来实现key-value的存储,而Entry又是ThreadLocalMap的内部类

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

从这段代码可以看出Entry的key就是ThreadLocal,而value就是线程变量的值。并且Entry继承WeakReference,表明Entry所对应key(ThreadLocal对象)的引用为一个弱引用

  • Java「四种引用类型」

    • 「强引用」:new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
    • 「软引用」:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
    • 「弱引用」:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
    • 「虚引用」:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
  • 总结

    「每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,而ThreadLocalMap由Entry型的数组组成,即每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。」

4.4  ThreadLocalMap与普通Map的区别.

分析到这里我们可以总结一下ThreadLocalMap与普通Map的区别

  • 「ThreadLocalMap没有实现Map接口;」
  • 「ThreadLocalMap没有public的方法, 只有一个default的构造方法, 因此ThreadLocalMap的方法仅仅只能在ThreadLocal类中调用, 属于静态内部类」
  • 「ThreadLocalMap的Entry实现继承了WeakReference,对ThreadLocal是一个弱引用」
  • 「ThreadLocalMap用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry」

五、ThreadLocal与ThreadLocalMap源码分析

ThreadLocal的核心源码其实就是前面说的四个方法【get、set、remove、initialValue】,一个内部类【ThreadLocalMap】的实现

「方法名」 「描述」
ThreadLocal() 创建ThreadLocal对象
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public T remove() 移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC
protected T initialValue() 返回当前线程局部变量的初始值
static class ThreadLocalMap 内部类

「ThreadLocal本质上是一个操作ThreadLocalMap的工具类」,因此在分析ThreadLocal源码的时候,会按照调用链深入ThreadLocalMap中分析具体的实现。

5.1 ThreadLocal之get方法

ThreadLocal的get方法,是通过当前线程去获取线程变量的方法。其源码如下

public T get() {
//先获取当前线程
Thread t = Thread.currentThread();
//通过当前线程拿到ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//当ThreadLocalMap对象不为空时,调用ThreadLocalMap对象的getEntry对象,获取当前线程的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//当Entry对象不为空时,获取其value返回
T result = (T)e.value;
return result;
}
}
// 如果没找就自己构建一个(会先判断ThreadLocalMap存在否),并且塞一个默认值null进去
//走到这里,说明ThreadLocalMap对象或者当前线程的Entry为空,调用setInitialValue方法,自己构建一个(会先判断ThreadLocalMap存在否),并且塞一个默认值null进去
return setInitialValue();
}
  • 「先获取当前的线程,调用getMap方法获取当前线程的ThreadLocalMap」
  • 「ThreadLocalMap不为空,调用ThreadLocalMap的getEntry方法获取当前线程对应的Entry」
  • 「ThreadLocalMap为空或者找不到目标Entry,则调用setInitialValue方法进行初始化」

5.2 ThreadLocal之setInitialValue方法

ThreadLocalMap为空或者找不到目标Entry时,调用setInitialValue方法进行初始化

private T setInitialValue() {
T value = initialValue(); //默认值null
//通过当前线程拿到ThreadLocalMap对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//ThreadLocalMap存在就将Entry丢进去
if (map != null) {
map.set(this, value);
} else {
//不存在就创建一个ThreadLocalMap
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
  • 「获取当前线程ThreadLocalMap,如果ThreadLocalMap为空,则创建一个新的ThreadLocalMap」

  • 「将当前的ThreadLocal作为key,null作为value,插入到新创建的ThreadLocalMap,并返回null」

    initialValue()方法为protected,如果想初始化时,将value设置为非空的默认值,可以通过子类继承ThreadLocal,重写setInitialValue方法。

ThreadLocal的get与setInitialValue方法流程图如下

一篇ThreadLocal走天下

5.3 ThreadLocalMap之getEntry方法

getEntry就是通过ThreadLocal其获取Entry中的值

private Entry getEntry(ThreadLocal<?> key) {
// //根据hash code与数组长度进行与运算 计算出索引位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
////如果该Entry非空,并且Entry的key和传入的key相等,则返回Entry
if (e != null && e.get() == key)
return e;
else
//否则调用getEntryAfterMiss方法,去索引后面的位置继续查找
return getEntryAfterMiss(key, i, e);
}
  • 根据hash code与数组长度进行与运算 计算出索引位置
  • 如果该索引位置Entry的key和传入的key相等,则为目标Entry,直接返回
  • 否则,说明e不是目标Entry,调用getEntryAfterMiss方法继续遍历。

「由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的。首先通过ThreadLocal的散列值找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回;去索引后面的位置继续查找」

一篇ThreadLocal走天下

5.4 ThreadLocalMap之getEntryAfterMiss方法

当获取到的entry不是目标entry时,会调用getEntryAfterMiss遍历数组查找entry时

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
//判断Entry的key是否为当前ThreadLocal,是则返回Entry
if (k == key)
return e;
//走到这里说明,Entry的key不是当前ThreadLocal,
if (k == null)
//判断Entry的key是否为空,为空调用expungeStaleEntry
expungeStaleEntry(i);
else
//不为空则计算下个索引位置
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
  • 从元素e开始向后遍历,如果找到目标Entry元素直接返回
  • 如果遇到key为null的元素,调用expungeStaleEntry方法进行清除
  • 当遍历到Entry为null时,结束遍历,返回null。
一篇ThreadLocal走天下

5.5 ThreadLocalMap之expungeStaleEntry方法

expungeStaleEntry是清除无效的Entry的方法

   private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除tab[staleSlot]位置Entry的数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// 清除之后重新计算 直到遇到Entry为null
Entry e;
int i;
//向后遍历,直到遇到Entry为null
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果当前Entry的key为空,则清除对象信息
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//重新计算Entry的索引位置,新位置 != 当前位置,则判断是否需要换位
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
  • 从staleSlot开始,清除key为null的Entry
  • 由于清空了Entry,Entry后面的索引需要重新计算,将不为空的元素放到合适的位置,最后遍历到Entry为空的元素时,跳出循环返回当前索引位置。
一篇ThreadLocal走天下

5.6 ThreadLocal之get总结

到这里ThreadLocal之get方法的整个调用链流程就清楚了。其主要的流程如下

  • ThreadLocal的get方法没有获取到ThreadLocalMap会调用ThreadLocal的setInitialValue进行初始化
  • ThreadLocal的setInitialValue优惠调用ThreadLocal的createMap方法创建ThreadLocalMap
  • ThreadLocal的get方法获取到ThreadLocalMap,则会调用ThreadLocalMap的getEntry方法获取Entry
  • ThreadLocalMap的getEntry如果没有找到Entry则会调用ThreadLocalMap的getEntryAfterMiss方法
  • ThreadLocalMap的getEntryAfterMiss会查找相应的Entry,同时调用ThreadLocalMap的expungeStaleEntry方法
  • ThreadLocalMap的expungeStaleEntry会清除无效的Entry
一篇ThreadLocal走天下

由于图片比较大,这里看起来有些模糊,如果需要原文件,请关注公众号【星河之码】,回复ThreadLocal,获取processon源文件

5.7 ThreadLocal之set方法

ThreadLocal中的set方法就是我们常用的向ThreadLocal存值的方法,先来看源码

public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//当前线程的ThreadLocalMap对象不为空,则存放value
map.set(this, value);
} else {
//当前线程的ThreadLocalMap对象为空,则先创建ThreadLocalMap后存放value
createMap(t, value);
}
}

其实现很简单,主要是依赖于ThreadLocalMap来实现

  • 先获取当前线程,再调用getMap方法拿到当前线程的ThreadLocalMap对象
  • 如果ThreadLocalMap不为空,则将当前ThreadLocal作为key,传入的值作为value,调用ThreadLocalMap的set方法
  • 如果ThreadLocalMap为空调用createMap创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap
一篇ThreadLocal走天下

5.8 ThreadLocal之createMap方法

ThreadLocal的createMap就是通过构造方法创建了一个ThreadLocalMap对象

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 调用ThreadLocalMap的构造方法
  • 创建一个数组,并创建一个Entry存储Value
  • 给数组设置一个阈值
一篇ThreadLocal走天下

5.9 ThreadLocalMap之set方法

ThreadLocalMap的set方法就是用来向ThreadLocalMap的Entry数组中存放新的Entry,下面来看看源码具体的实现

private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;

// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(走到这里说明 e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,则new一个Entry
tab[i] = new Entry(key, value);
// 表示这个ThreadLocalMap里面增加一个ThreadLocal
int sz = ++size;
// cleanSomeSlots 清楚陈旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

通过源码的分析,ThreadLocalMap之set方法的实现主要可以分为以下几个步骤:

  • 「通过传入的key的hashCode计算出索引的位置」
  • 「从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置」
  • 「如果找到某个Entry的key和传入的key相同,则用传入的value替换掉该Entry的value。」
  • 「如果遍历到某个Entry的key为空,则调用replaceStaleEntry方法」
  • 「如果通过nextIndex寻找到一个空位置(代表没有找到key相同的),则将元素放在该位置上」
  • 「调用cleanSomeSlots方法清理key为null的Entry,并判断是否需要扩容,如果需要则调用rehash方法进行扩容」
一篇ThreadLocal走天下

通过源码的分析,发现其实「集合Map和ThreadLocalMap的Entry虽然都是key-value结构,但是他们的set()方法和集合的put方法的实现在处理散列冲突的方式上有很大的不同:」

  • 「集合Map的put()采用的是拉链法」
  • 「ThreadLocalMap的set()则是采用开放定址法」

线性探测法:

通过散列函数hash(key),找到关键字key在线性序列中的位置,如果当前位置已经有了一个关键字,就产生了哈希冲突,此时会往后探测i个位置(i小于线性序列的大小),直到当前位置没有关键字存在。

5.10 ThreadLocalMap之replaceStaleEntry方法

「replaceStaleEntry方法是用来标记Entry数组是否要清理的,在调用Set方法时,会遍历数组,根据数组中entry的key是为空判断是否要清除该Entry」

一篇ThreadLocal走天下
/**
* key : 当前的ThreadLocal
* value : 线程副本的值
* staleSlot:遇到的第一个旧数据的索引(即对应的entry 的 k= null)。
*/

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot)
{
Entry[] tab = table;
int len = tab.length;
Entry e;
//标记 slotToExpunge前面的元素是不需要清除的
int slotToExpunge = staleSlot;
//向前遍历,直到遇到Entry为null
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//记录最后一个key为null的索引位置
if (e.get() == null)
slotToExpunge = i;
//向后遍历,直到遇到Entry为null
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//k == key说明是同一个线程的ThreadLocal,更新当前Entry的value为入参的value
if (k == key) {
e.value = value;
//将索引位置i和staleSlot的元素互换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//相等代表向前遍历寻找未找到key=null,即staleSlot前没有需要清除的元素,
//因为原staleSlot的元素被放到i位置,这时位置i前面的元素都不用清除
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//从slotToExpunge位置开始清除key为null的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果第一次遍历到key为null的元素,并且上面的向前寻找未找到key为null
//则将slotToExpunge设置为当前的位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//如果key没有找到,新建一个放到staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//不相等代表staleSlot位置还有其他位置的元素需要清除(key为null代表需要清除)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

通过源码分析,我们知道这个方法主要的作用就是,「将需要清除的Entry通过替换的方式,放到slotToExpunge位置的后面」

  • slotToExpunge作为一个Entry数组的分界线,下标在slotToExpunge后面的元素是需要被清除的

  • 以staleSlot为起点,向前遍历,找到最后一个Entry的key为null的位置,并将i位置和slotToExpunge位置的元素对换,当遇到Entry为空跳出循环

    这个时候找到的是要清除的位置,key为null说明没有引用的了,遍历完成之后说明在I之前的位置都要清除

  • 以staleSlot为起点,向后遍历,如果遍历到key和入参key相同,则将入参的value替换掉该Entry的value,并将i位置和slotToExpunge位置的元素对换,当遇到Entry为空跳出循环

  • 如果没有找到key,则使用入参的key和value新建一个Entry,放在staleSlot位置

  • 判断是否还有其他位置的元素key为null,如果有则调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素

一篇ThreadLocal走天下

5.11 ThreadLocal之rehash方法

ThreadLocal的rehash是一个扩容的方法,在set的时候调用


private int threshold; // Default to 0

private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private void rehash() {
//先清除key为null的Entry
expungeStaleEntries();
//如果清除之后数组长度超过阀值(len*2/3)的3/4,则调用resize扩容
if (size >= threshold - threshold / 4)
resize();
}

resize方法

private void resize() {
Entry[] oldTab = table;
//获取旧的数组长度
int oldLen = oldTab.length;
//新的长度为原来的2倍
int newLen = oldLen * 2;
//创建新的Entry数组
Entry[] newTab = new Entry[newLen];
int count = 0;
//遍历旧数组值计算新下标放入新数组
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//如果key为null,则清空value 等待GC
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
//如果该位置已经存在元素,则重新寻址
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//设置新数组的扩容阀值
setThreshold(newLen);
size = count;
table = newTab;
}

ThreadLocalMap的扩容主要分为以下几步:

  • 扩容之前先调用expungeStaleEntries方法清理key为空的Entry
  • 清理后判断Entry数组的长度,如果长度超过阈值的3/4,则调用resize进行扩容
  • 设置新的Entry数组的长度是旧数组的两倍
  • 遍历旧数组,如果key为null,将value清空等待GC
  • 如果key不为null,重新通过hash code计算元素在新数组的索引位置,如果新的索引位置已经有值,则调用nextIndex方法寻找空位置,将元素放在新数组的对应位置。
一篇ThreadLocal走天下

5.12 ThreadLocal之set总结

到这里ThreadLocal之set方法的整个调用链流程也就清楚了。其主要的流程如下

  • 「ThreadLocal的set方法没有获取到ThreadLocalMap会调用ThreadLocal的createMap进行创建」
  • 「获取到ThreadLocalMap,则会调用ThreadLocalMap的set方法」
  • 「ThreadLocalMap的set方法时,遍历Entry数组,如果没有找到对应的key,则新建一个Entry放入数组中并且调用ThreadLocalMap的rehash进行扩容」
  • 「如果找到entry的key为null则调用ThreadLocalMap的replaceStaleEntry方法对数组进行整理,标记要清除的Entry」
  • 「ThreadLocalMap的expungeStaleEntry方法整理后调用ThreadLocalMap的expungeStaleEntry方法执行清除」
  • 「ThreadLocalMap的rehash方法在扩容之前会先调用ThreadLocalMap的expungeStaleEntries方法清理key为空的Entry」
  • 「清理之后判断数组长度是否大于阈值的3/4,超过则调用ThreadLocalMap之resize方法执行扩容」
一篇ThreadLocal走天下

由于图片比较大,这里看起来有些模糊,如果需要原文件,请关注公众号【星河之码】,回复ThreadLocal,获取processon源文件

5.13 ThreadLocal之remove方法

ThreadLocal的remove是清除ThreadLocal的方法,这个方法很重要,在我们用完变量之后一定要记得调用ThreadLocal的remove方法,将变量删除,不然可能会造成OOM。

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

通过上述源码发现,ThreadLocal的remove方法实现很简单,就是调用了一下ThreadLocalMap的remove方法,继续看ThreadLocalMap的源码

private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算当前ThreadLocal在ThreadLocalMap的下标
int i = key.threadLocalHashCode & (len-1);
//从下标开始遍历数组,查找这个threadlocal
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果是当前threadlocal
if (e.get() == key) {
//把对这个entry的引用置为空,操作后e.get无法取到key
e.clear();
//调用expungeStaleEntry,将e.get()==null的Entry从数组中清除
expungeStaleEntry(i);
return;
}
}
}

通过上述段源码的分析,remove大致可以分为以下几个步骤

  • 获取当前线程的ThreadLocalMap,ThreadLocalMap不为空,则调用其remove,并传入当前的ThreadLocal对象
  • 将key为当前ThreadLocal的键值对移除,并且会调用expungeStaleEntry方法清除key为null的Entry。
一篇ThreadLocal走天下

5.14、ThreadLocal源码分析总结

以上主要对ThreadLocal的get,set、remove方法进行了源码分析,其主要流程如下

一篇ThreadLocal走天下

由于图片比较大,这里看起来有些模糊,如果需要原文件,请关注公众号【星河之码】,回复ThreadLocal,获取processon源文件

六、ThreadLocal为什么会内存泄漏

6.1 问题分析

通过前面的介绍,我们知道「每个Thread都会维护一个ThreadLocal.ThreadLocalMap的map对象,这个map的key为ThreadLocal实例,并且是一个弱引用,而弱引用有利于GC回收」

一篇ThreadLocal走天下

通过上图的结构,当我们在程序中释放了ThreadLocal,即ThreadLocal == null时,GC就会回收这部分空间,此时map中的Key==null,而value还存在着强引用,因为ThreadLocalMap是依附在Thread上的,只有thead线程退出以后,value的强引用链才会断掉。

此时在线程退出之前始终会存在一条强引用的引用关系,这些key为null的Entry的value就会一直存在一条强引用链:

Current Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

「实际上我们实现线程大多数都是使用的线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露」

  • 「总结内存泄漏根本原因」

    「由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于线程池中复用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏」

6.2 解决方法

解决方法内存泄漏很简单:

「使用完ThreadLocal之后,一定要记得调用remove()方法,避免内存泄漏」

通过上述对ThreadLocal的remove()源码分析知道,ThreadLocal的remove()内部实现就是调用 ThreadLocalMap 的remove方法,它会把Entry中的key和value都设置成null,使其能够被GC及时回收

七、ThreadLocal是如何解决hash冲突

7.1 hash冲突产生

在上述分析ThreadLocalMap时,在其构造方法,get,set方法频繁的用到以下一行代码来计算Entry数组下标,所以ThreadLocal的hash冲突肯定也是跟这行代码有关系

int i = key.threadLocalHashCode & (table.length - 1);

这行代码可以分为两部分key.threadLocalHashCode与table.length – 1

  • 「threadLocalHashCode」

    先来看看threadLocalHashCode是如何实现的

    一篇ThreadLocal走天下

    同过源码发现,「每次获取threadLocalHashCode是通过一个个AtomicInteger类型的当前值加上HASH_INCREMENT(0x61c88647),而HASH_INCREMENT = 0x61c88647满足斐波那契数列(黄金分割数),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 尽量避免hash冲突,这是也是Entry[]的初始容量为16的原因之一」

  • 「key.threadLocalHashCode & (table.length – 1)」

    「通过threadLocalHashCode计算出hashCode后进行hashCode & (size – 1)运算,能够保证在索引不越界的前提下,使得hash发生冲突的次数减小」

7.2 解决哈希冲突

还是在ThreadLocalMap的get,set等方法中,可以频繁的看到一个for循环

for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
.... 其他代码省略
}

这个for循环就是在查找Entry中的元素,其中调用了一个nextIndex方法,来计算下一个元素的下标

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

通过nextIndex方法的源码,我们发现它就是在数组长度范围之内i + 1,挨个往下找,到头了就返回0,重新开始

到这里我们可以总结出来,

「ThreadLocalMap使用【线性探测法(一条线挨个往下的找)】来解决哈希冲突的,调用一次nextIndex方法探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出」

比如:当前Entry[]的长度就是默认的16,假设key.threadLocalHashCode & (table.length – 1)计算的结果为14,如果table[14]上已经有值,并且其key与当前key不一致,测试就发生了冲突,此时就通过nextIndex将14加1得到15,取table[15]进行判断,如果table[15]也发生Hash冲突,则取table[0],以此类推,直到可以插入

由于ThreadLocalMap使用线性探测法,来解决散列冲突,所以我们可以把Entry[]看成一个环形数组

八、 为什么不用Thread作为ThreadLocalMap的key

为什么不用Thread作为ThreadLocalMap的key,而是现在的ThreadLocal作为Key?其实本质上可以的,而且在JDK8之前还就是这么做的,只是JDK8做了调整,看下面这张图

一篇ThreadLocal走天下

通过这种图可以看出来,「JDK8之前就是用Thread作为ThreadLocalMap的key,而JDK8改成ThreadLocal作为Key」,为什么这么改呢?

  • 「一个线程可能有多个私有变量,Thread作为key,还需要另外标识Value,这样就可能导致Map体积膨大,导致性能下降」

    比如在ThreadLocalMap放在一个对象中,通过name在对象中获取Value,相当于套了一层

  • 上图可以看到「JDK8之前map是由ThreadLocal维护的,也就是说ThreadLocal销毁ThreadLocalMap才会销毁,而JDK8的ThreadLocalMap是随着线程的消亡而消亡,能减少内存的使用」

九、key为什么设置弱引用

ThreadLocalMap的key为什么设置弱引用,我们通过以下三点来看

  • 「key设置强引用有什么问题」

    如果设置为强引用,当使用完ThreadLocal后,GC回收时,threadLocalMap中Entry强引用了threadLocal,导致ThreadLocal无法被回收。只要没有调用remove()或者CurrentThread没有退出之前,会一直存在一条引用链

    CurrentThread Ref → CurrentThread → ThreadLocalMap-> entry

    因为这条引用链,GC就无法回收Entry,导致内存泄漏,所以 「ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的」

  • 「key设置弱引用好处是什么」

    ThreadLocalMap的key设置弱引用好处是:「避免内存泄漏」

    • 「ThreadLocalMap的key设置弱引用,当ThreadLocal使用完,GC要回收时,由于时弱引用,Entry不会影响ThreadLocal的回收。」
    • 「当ThreadLocal回收后,Entry的key就是null,而在前面的get,set等方法的源码分析知道,这些方法会将key==null对应的Entry的value置为 null,这样即使CurrentThread没有退出,忘记调用remove,GC也可以回收相应的Entry(key==null,value==null),从而避免内存泄漏」
  • 「ThreadLocalMap的value为什么不设置弱引用」

    「threadLocal的作用就是将线程变量value副本化,说明这个变量副本就是当前线程一个强引用,如果设置一个弱引用,只要jvm执行一次gc操作,这个变量副本就被回收了,此时再去ThreadLocalMap取值的时候就是一个null了」

十、ThreadLocal使用建议

基于以上的分析,我们在使用ThreadLocal的时候可以有意识的做一些规范

  • 「同一个线程中尽量不用使用过多的ThreadLocal,多个ThreadLocal放入ThreadLocalMap中时会增加Hash冲突的可能」
  • 「每次使用完ThreadLocal,一定调用它的remove()方法,清除数据」,这是重点。


原文始发于微信公众号(星河之码):一篇ThreadLocal走天下

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/60894.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!