ThreadLocal

一、什么是ThreadLocal

ThreadLocal用于保存线程全局变量,以方便调用。即,当前线程独有,不与其他线程共享;可在当前线程任何地方获取到该变量。

二、ThreadLocal的使用

1、如何保存内容

ThreadLocal实例,并调用set函数,保存中国字符串,分别在当前线程和new-thread线程获取该值。通过打印结果可以看到,虽然引用的是同个对象,但new-thread线程获取到的值却是null

use

运行结果:

1
2
main 中国
MainActivity: new-thread null

这是什么情况呢?

ThreadLocalset函数中,获取当前线程的ThreadLocalMap实例,如何当前线程第一次使用ThreadLocal,则需要创建ThreadLocalMap实例,否则直接通过ThreadLocalMap实例的set函数进行保存。

set

2、如何获取内容

由于main线程前面set函数将内容保存到ThreadLocalMap实例中,已经可以获取到中国字符串。而在new-thread线程中,由于是第一次使用ThreadLocalMap,所以此时mapnull,并调用setInitialValue函数。

get

setInitialValue函数中,调用了initialValue函数,该函数直接返回了null,这就是为什么在new-thread线程获取的值是null。因此setInitialValue函数主要为当前线程创建ThreadLocalMap对象。

setInitialValue

3、ThreadLocalMap

ThreadLocalMap内部持有一个数组table,用于保存Entry元素。Entry继承至WeakReference,并以ThreadLcoal实例作为key,和保存内容 T作为value。当发生GC时,key就会被回收,从而导致该Entry过期。

Entry

每一个线程都持有一个ThreadLocalMap局部变量threadLocas,如下图所示。

image-20210118113312896

3.1 ThreadLocalMap的创建

ThreadLocalMap对象的创建,也就是ThreadLocal 对象调用了自身的createMap函数。

createMap

ThreadLocalMap的构造函数,创建了一个保存Entry对象的table数组,默认大小INITIAL_CAPACITY=16。并通过threadLocalthreadLocalHashCode属性计算出Entry对象在数组的下标,然后进行保存,并计算出阈值INITIAL_CAPACITY的2/3=16。

threadLocalHashCode属性在ThreaLocal对象创建时会自动计算得出。

threadLocalHashCode

threadLocalHashCode在不同的ThreadLocal实例中是不同的,通过nextHashCode.getAndAdd函数计算出下一个实例的threadLocalHashCode值,而第一个ThreadLocalthreadLocalHashCode值则是从0开始,与下一个threadLocalHashCode间隔HASH_INCREMENT

通过threadLocalHashCode & (len-1)计算出来的数组下标,分布很均匀,减少冲突。但是呢,冲突时还是会出现,如果发生冲突,则将新增的Entry放到后侧entry=null的地方。

三、源码分析

1、ThreadLocalMap的set函数

在上一节中,分析了ThreadLocal实例的set函数,最终是调用了ThreadLocalMap实例的set函数进行保存。

mapset

通过代码分析可知,ThreadLocalMapset函数主要分为三个主要步骤:

  1. 计算出当前ThreadLocaltable数组的位置,然后向后遍历,直到遍历到的Entrynull则停止,遍历到Entrykey与当前threadLocal实例的相等,直接更替value;

  2. 如果遍历到Entry已过期(Entrykeynull),则调用replaceStaleEntry函数进行替换。

  3. 在遍历结束后,未出现1和2两种情况,则直接创建新的Entry,保存到数组最后侧没有Entry的位置。

在第2步骤和最后都会清理过期的Entry,这个稍后分析,先看看第2步骤,在检测到过期的Entry,会调用replaceStaleEntry函数进行替换。

replaceStaleEntry

replaceStaleEntry函数,主要分为两次遍历,以当前过期的Entry为分割线,一次向前遍历,一次向后遍历。

在向前遍历过程,如果发现有过期的Entry,则保留其位置slotToExpunge,直到有Entrynull为止。这里只是判断staleSlot前方是否有过期的Entry,然后方便后面进行清理。

在向后遍历过程,如果发现有key相同的Entry,直接与staleSlot位置的Entry交换value(上图注释有问题)。如果没有碰到相同的key,则创建新的Entry保存到staleSlot位置。与此同时,如果向前遍历没有发现过期Entry,而在向后遍历发现过期的ntry,则需要更新过期位置slotToExpunge,因为后面的清除内容是需要slotToExpunge

2、ThreadLocalMap清除过期Entry

在上一小节中,会通过expungeStaleEntry函数和cleanSomeSlots函数清理过期的Entry,它们又是如何实现呢?

expungeStaleEntry函数清理过期Entry过程被称为:探测式清理。函数传递进来的参数是过期的Entry的位置,工作过程是先将该位置置为null,然后遍历数组后侧所有位置的Entry,如果遍历到有Entry过期,则直接置null,否则将它移到合适的位置:hash计算出来的位置或离该hash位置最近的位置。

expungeStaleEntry

经过这么一次经历,staleSlot位置到后侧最近entry=null的位置就不存在过期的entry,而每个entry要么在原有hash位置,要么离原有hash位置最近。

expungeStaleEntry函数的工作范围:

expungeStaleEntry (1)

expungeStaleEntry函数一开始会将起点,即数组第3的位置设置为null。然后开始遍历数组后侧元素,4和5位置无论是否在它的hash位置,在这里都保持不变。遍历到第6时,发现entry已过期,将第6设置为null。此时3和6位置变成白色了。

image-20210116175944199

A、遍历到第7的时候,假设h != i成立,那么第7位置的entry将被移到第6位置,空出第7位置。

image-20210116180006575

B、接着遍历到第8位置,假设h != i不成立,则第8的entry的位置不变。

接着继续遍历后侧元素,重复着A和B步骤,直到碰到entry为null,退出遍历。例如这里的第10位置,entry=null。

由于探测性清理,碰到entry=null的情况就会结束。而通过cleanSomeSlots函数进行启发式清理,碰到entry=null不停止,而是由控制条件n决定,而在这个过程中,碰到过期entry,n又恢复到数组长度,加大清理范围。

clean

在启发式清理过程,如果碰到过期Entry,会导致控制条件n恢复到数组长度len,从而导致循环次数增加,则往后nextIndex次数增加,从而增加清理范围。这种方式也不一定能完整清理后面所有过期元素,例如在控制n右移所有过程中,没有碰到过期的entry,就结束了。

3、ThreadLocalMap的扩容机制

在第1节,调用ThreadLocalMapset函数最后,会调用reHash函数进行扩容。

rehash

在外层进行启发式清理后,如果size>threshold则会进行rehash,而在rehash中,会清理整个数组的过期Entry,如果清理后,数组长度还大于3/4*threshod,则进行扩容resize

resize

resize函数直接创建新的数组,长度为旧数组的两倍。然后重新计算旧数组元素在新数组的位置,复制。

四、内存泄露

正常情况下,不再使用ThreadLocal对象,将其将强引用设置为null,在发生GC时,ThreadLocal对象就会被回收。如果线程还存活(例如线程池线程的复用),就会导致Entryvalue对象得不到释放,会造成内存泄露。所以,在使用完ThreadLocal实例后,调用remove函数清除一下。

疑惑

发生GC的时候,Key会被回收么,还能获取到值么?

正常情况下,ThreadLocal实例同时被强引用所引用,所以在发生GC的时候,是不会回收的,也就是此时WeakReference.get是有返回值的,不会被回收,直到我们设置强引用指向为null,此时就不存在强引用,会被垃圾回收。

gc

image-20210118175757028

推荐阅读:Java引用与ThreadLocal