前言
ThreadLocal是一个平时Android开发中并不常见的类,正因为少接触,所以对它的了解并不多。但实际上,它对我们常用的Handler通信机制起着重要的支撑作用。ThreadLocal,顾名思义,线程封闭的变量,也即该变量的作用范围是以当前线程为单位,别的线程不能访问该变量。ThreadLocal
简单的例子
public class ThreadLocalPractice {
public static void main(String args[]){
ThreadLocal integerThreadLocal = new ThreadLocal<>();
integerThreadLocal.set(1);
new Thread(() -> {
integerThreadLocal.set(2);
System.out.println("Thread ID:" + Thread.currentThread().getId() + ",integerThreadLocal = " + integerThreadLocal.get());
}).start();
new Thread(()->{
//do nothing
System.out.println("Thread ID:" + Thread.currentThread().getId() + ",integerThreadLocal = " + integerThreadLocal.get());
}).start();
System.out.println("Thread ID:" + Thread.currentThread().getId() + ",integerThreadLocal = " + integerThreadLocal.get());
}
}
运行程序,观察结果如下:
在线程1中,我们设置了threadlocal的值为1,然后在子线程中设置为2以及不做任何修改,得到的结果分别是1、2和null,这说明了ThreadLocal的作用域限制在了某一线程中,是线程封闭了,一个线程的ThreadLocal的值改变了,并不影响另一条线程的ThreadLocal的值。下面,我们就从源码的角度来分析它的工作原理。
源码分析
注意:下面源码来自JDK-10。
1、几个关键的类或对象
在真正阅读源码之前,笔者先列举出几个关键点,以便分析的时候能更容易理解源码。
①ThreadLocal.ThreadLocalMap 这是ThreadLocal的一个静态内部类,它本质上是一个Hash Map,是专门为维护线程私有的变量而定制的。
public class ThreadLocal {
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private void set(ThreadLocal> key, Object value) {
//...
}
private Entry getEntry(ThreadLocal> key) {
//...
}
}
}
从上面的源码可以看出,ThreadLocalMap与HashMap结构上是相似的,都有初始容量、都用数组来装载value值、都有阈值和负载因子等,它们都是利用了散列算法而做的散列表。不同之处在于ThreadLocalMap是基于线性探测的散列表,而HashMap是基于拉链法的散列表。
我们关注上面的Entry[] table
这一成员变量,这是一个Entry
数组,它保存了一系列的通过调用ThreadLocal#set(T value)
方法而传递进来的value值。
②ThreadLocalMap.Entry 这是ThreadLocalMap的一个静态内部类,可以看一下它的源码:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
它继承自WeakReference,泛型参数是ThreadLocal>,同时有一个Object的变量,这说明了该Entry持有一个对ThreadLocal的弱引用,同时把ThreadLocal保存的值放到了这里的Object对象内保存起来。
③Thread类的ThreadLocalMap成员变量:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
//other code...
}
从源码注释可以看出,ThreadLocalMap是属于当前Thread对象的(因为不同线程所对应的Thread对象不同),所以ThreadLocalMap仅在当前Thread内起作用,不同的线程都维护着各自的ThreadLocalMap,相互之间没有联系。
2、解析ThreadLocal.set(T value)方法
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前的线程
ThreadLocalMap map = getMap(t); //根据当前线程获取对应的Map
if (map != null)
map.set(this, value); //2、把value放进map中
else
createMap(t, value); //1、创建一个Map
}
上面的流程很简单,就是根据线程对象来获取到线程内部维护的ThreadLocalMap对象,然后再把值放到map内部。
2.1、我们先看createMap(t,value)方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里把Thread对象的map变量进行了实例化,指向一个ThreadLocalMap对象。这里可以知道线程内部维护的threadLocalMap变量只有在进行第一次保存ThreadLocal变量时才会进行实例化,也即是常说的延迟初始化。
我们接着去看看ThreadLocalMap的构造方法做了什么工作:
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算经过散列之后的数组下标
table[i] = new Entry(firstKey, firstValue); //Entry内部的object保存了value值
size = 1;
setThreshold(INITIAL_CAPACITY);
}
从上面的源码可以看出,数组的下标通过散列函数计算来得到,即firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
,这里相当于把哈希值对数组长度取模运算,那么ThreadLocal是怎么确定自身的哈希值的呢?我们循着源码的踪迹继续向前探索,我们来看看threadLocalHashCode
到底是何方神圣:
public class ThreadLocal {
//成员变量,声明为final域,一旦赋值便不能修改
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT); //不断自增HASH_INCREMENT大小
}
//other code...
}
观察上面的源码,threadLocalHashCode
是ThreadLocal
我们把关注点放在nextHashCode()方法,它是一个静态方法,与它相关的一个变量是nextHashCode
,这是一个AtomicInteger,即原子变量,它也是一个静态变量。我们知道,静态变量是属于类所有的,与类的某一对象实例无关,所以通过不断的实例化ThreadLocal类,它的静态变量nextHashCode
静态变量就会不断地自增,并且每次都自增0x61c88647
。通过这种方法,不同的ThreadLocal实例便获得了一个独特的哈希值(注:由于采用了原子变量,在多线程环境下也能获得正确的取值)。
2.1-小结:上面分析了createMap(t,value)方法,通过该方法,实例化了一个与当前线程有关的ThreadLocalMap实例,并且通过ThreadLocal实例化时获得的一个哈希值与Entry[]数组的长度进行与运算来算出下标i,并把value保存到Entry[]数组的该下标位置处。
2.2、我们继续来分析map.set(this, value)
现在,让我们把目光放回刚才的set()方法上,当map不是空值时,会调用map.set()函数进行插入操作。下面,我们来看看map.set()方法做了什么工作:
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //根据threadLocal的hashcode来获取散列后的下标i
//在i的基础上,不断向前探测,即线性探测法。探查是否已经存在相应的key,如果存在旧替换。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { //nextIndex()的作用在于自增i值,如果超过数组长度就变成0,相当于把数组看出环形数组
ThreadLocal> k = e.get(); //获取Entry持有的ThreadLocal弱引用
if (k == key) { //如果两个ThreadLocal相等,表示需要更新该key对应的value值
e.value = value;
return;
}
if (k == null) { //如果k为null,表示Entry持有的弱引用已经过期,即ThreadLocal对象被GC回收了
replaceStaleEntry(key, value, i); //此时更新旧的Entry值
return;
}
}
tab[i] = new Entry(key, value); //如果走到了这里,表示插入的key-value是新值
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) //当ThreadLocalMap达到了一定的容量时,需要进行扩容
rehash();
}
上面的注释已经说得很清楚了,主要流程就是先通过散列找到应该存放的数组下标index,然后利用线性探测的方法逐步增大index,观察对应Entry[]位置是否存在Entry对象,然后选择替换或者实例化一个新的Entry对象。
3、解析ThreadLocal.get()方法
上面讲述了set()方法,那么当我们调用get()方法来获取一个值的时候,背后所作的工作又是怎样的呢?
public T get() {
Thread t = Thread.currentThread(); //获取当前线程对象
ThreadLocalMap map = getMap(t); //根据线程对象获取其内部的Map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //如果Map尚未初始化,则初始化
}
逻辑很简单,就是获取到当前线程所维护的一个ThreadLocalMap,然后以当前ThreadLocal对象作为key来获取一个Entry,具体逻辑我们看map.getEntry(this)
:
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1); //通过散列函数计算数组下标
Entry e = table[i];
if (e != null && e.get() == key) //如果直接命中,则返回
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { //利用线性探测法来寻找key所在的位置
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null) //如果当前遍历到的key已经被回收了,那么进行清理
expungeStaleEntry(i);
else
i = nextIndex(i, len); //利用环形数组的原理来变化i值
e = tab[i];
}
return null;
}
逻辑还是很清晰的,如果通过散列函数得到的数组下标直接命中key值,那么可以直接返回,否则进一步调用getEntryAfterMiss(key,e)
方法来进行线性探测查找key。
值得注意的是,这里把查找过程分成了两个方法来处理,为什么要这样做?从源码的注释可以看出,这样设计的目的是最大限度提高getEntry(key)
方法的性能,也即是提高直接命中时的返回结果的效率。这是因为JVM在运行的过程中,如果一些短函数被频繁的调用,那么JVM会把它优化成内联函数,即直接把函数的代码融合进调用方的代码里面,这样省掉了函数的调用过程,效率也会得到提高。
4、ThreadLocalMap处理已失效的Key的过程
ThreadLocalMap是ThreadLocal的核心部分,其中大量逻辑都是在ThreadLocalMap中完成的,所以其重要性不言而喻。因此值得我们来继续学习它的优秀思路。
我们通过上面的学习,知道了Entry
持有对ThreadLocal
的弱引用,但同时它也持有一个对Object
的强引用,前者是key,后者是value。那么随着系统的运行,ThreadLocal
可能会被GC回收了,那么此时Entry
持有的key值就变成了失效的值。因此,在get()和set()的过程中,ThreadLocalMap可能会触发对已失效key的处理,以回收空间。
4.1、我们来看看ThreadLocalMap.expungeStaleEntry(int)
的源码:
private int expungeStaleEntry(int staleSlot) { //这里传入的staleSlot表示这个下标位置的Entry是失效的
Entry[] tab = table;
int len = tab.length;
//把当前位置Entry的value值置空,同时也把Entry[staleSlot]置空,便于GC回收
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//线性探测法进行环形探测,回收失效的key值及Entry,对于没失效的Entry进行ReHash得到h,
//再把该Entry放到对h线性探测的下一个为空的位置
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
经过调用一次i = expungeStaleEntry(staleSlot)
后,staleSlot到i之间的无效值都被清理了,并且在这其中的有效Entry
也被再散列到了相应的位置。
4.2、在理解了expungeStaleEntry(staleSlot)
的作用之后,我们接下来看看与之关系密切的另一个方法ThreadLocalMap.cleanSomeSlots(int i, int n)
方法:
/**
* 启发式地对Entry[]进行扫描,并清理无效的slot.
* 从下面的while循环表达式可以知道,第一次扫描的单元是i ~ i+log2(n),
* 如果在这期间发现了无效slot,那么把n变大到数组的长度,此时扫描单元数为log2(length)。
* 即,在扫描的期间,如果发现了无效slot,就不断增大扫描范围。因此称之为启发式扫描。
*
* @param i 无效slot所在的位置
* @param n 控制扫描的数组单元数的参数
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len); //线性探测向前环形扫描
Entry e = tab[i];
if (e != null && e.get() == null) { //如果找到一个无效Entry(Key被回收)
n = len; //设置n为Entry[]的长度,以增加扫描单元数
removed = true;
i = expungeStaleEntry(i); //调用清理函数,i就是下一次向前探测的初始位置,
//因为在[旧i,新i]之间的无效slot都被清理了
}
} while ( (n >>>= 1) != 0); // n >>>= 1 表示 n = n >>> 1,>>>表示无符号右移
return removed;
}
代码给出了详细的注释,cleanSomeSlots(int,int)
就是一个启发式的过程,在给定范围内如果没有找到失效的Entry
,那么就停止搜索,否则会不断增大搜索范围。该方法避免了对Entry[]
的全部扫描,是时间效率和存在无效slot之间的一个折衷方案。
4.3、让我们回到2.2的代码处,在set(key,value)
方法内当扫描到的key是null时,会调用replaceStaleEntry(key, value, i)
方法进行替换,这时候未免产生了一个疑问:在当前位置进行替换,如果后面已经有相同的key但还没扫描到怎么办?其实,replaceStaleEntry(key, value, i)
方法已经帮我们解决了这个问题,我们来查看该方法的源码:
/**
* 在已知存在失效slot的情况下,插入一个key-value值。
* 该方法会触发启发式扫描,清理失效slot。
*
* @param key ThreadLocal实例
* @param value ThreadLocal实例需要保存的值
* @param staleSlot 一个失效Entry.key所在的下标
*/
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//向前扫描,寻找一个失效的slot,直到数组元素为null
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后扫描,直到找到一个key与参数的key相等的位置,
//或者遇到数组元素为null停止扫描
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
//如果找到相同的key,把i位置的Entry与staleSlot位置的Entry交换位置
//经过这一步骤,失效的slot被移到了i位置
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//从slotToExpunge位置开始启发式清理过程,该位置根据在前向扫描过程中
//是否找到另一个失效slot来决定,如果找到,则从该位置开始清理;
//否则,从i位置开始清理,即上面被交换了位置的slot。
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果前向扫描没有失效slot,并且在后向扫描的过程中遇到了第一个失效slot,记录下该位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//在staleSlot位置插入新值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//从失效slot位置进行启发式清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
该方法的核心就在于寻找一个合适的位置来放入给定的value值,并寻找合适位置来执行cleanSomeSlots(int,int)
方法。而扫描位置取决于前向扫描是否找到一个失效slot和后向扫描是否找到一个相等的key或者一个失效的slot。
4-4小结:经过上面的流程梳理以及源码分析,我们可以得知,触发cleanSomeSlots(int i, int n)
启发式清理有两个场景:①新的Entry被添加到数组中。②把失效key所在的slot替换成新的Entry。启发式清理的过程是在发现了失效slot的情况下会逐渐增大扫描单元,以获得较好的时间复杂度。expungeStaleEntry(staleSlotIndex)
则是关键的清理函数,它向前环形遍历,不断地清理失效key的Entry,置为null同时断开强引用,把有效的Entry再散列到别的位置,直到遇到null值。replaceStaleEntry(key,value,i)
则是在要替换Entry[]的某一元素时被调用,它会在i位置前后扫描查看是否有失效key的Entry,以触发一次启发式清理的过程。
总结
经过上面的学习,我们可以总结出下面的ThreadLocal UML类图如下:
本文主要探究了ThreadLocal和ThreadLocalMap的原理,以及ThreadLocalMap的清理失效Entry的算法,其中ThreadLocalMap是使用线性探测解决碰撞的哈希表的一个优秀实现例子,我们可以借鉴它的实现方法,让我们对哈希表的理解更加深入。
好了,本文到这里结束,谢谢你们的阅读!如果可以的话,点个赞再走吧~