ThreadLocalMap源码实现
ThreadLocalMap是Thread内部存储ThreadLocal的数据结构,本质上就是一个Map,不过它又和我们熟悉的java.util.map并不太相同,我们来了解一下ThreadLocalMap的具体实现。
1.内部存储结构
ThreadLocalMap的内部存储结构是一个Entry数组,但是它和hashMap不太一样,它没有next指针,说明它不是数组+链表的形式来解决hash冲突,具体往下看。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们来看一下这个Entry内部类的实现,它继承了弱引用WeakReference,key值为ThreadLocal的弱引用,注意,这里存储的ThreadLocal的弱引用,并不是ThreadLocal本身。value值为一个Object类型的强引用类型,用于存放我们需要让ThreadLocal存储的对象。
使用弱引用的原因
首先呢,我们采用普通的kv形式,我们并不能知道ThreadLocalMap外部的强引用是否断开,所以该Entry是否已经被我们弃用我们并不能确定,这样就造成了该Entry的生命周期与线程强绑定在一起,该节点也一直处于GC roots可达状态无法被回收直至线程消亡。即使外部强引用断开,对应的值被我们弃用,我们也无法知道,该无效的Entry无法被清理,这样就会造成资源的浪费。采用WeakReference的形式,我们先来看一下WeakReference的定义,若一个对象没有任何强引用对象绑定,那么该对象下一次GC时无论内存是否足够都会被回收,若某个ThreadLocal没有强引用可达,那么随着它被GC回收,对应的ThreadLocalMap中的Entry的key值也会随之失效,这样就等于给我们一个清理该无效Entry的信号,便于我们的清理工作。
2.ThreadLocalMap的参数
//Entry数组的初始大小 这里2的幂次 与hashMap类似
private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
//ThreadLocalMap中存储元素的数量
private int size = 0;
//阈值 判断扩容时需要该值
private int threshold; // 默认为0
ThreadLocalMap内部维护这一个初始大小为16的Entry数组,这里大小2的幂次不多做解释。
3.ThreadLocalMap解决hash冲突的办法
我们继续往下看:
//调整阈值 保持为负载因子的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//寻找下一个索引的位置(环形)
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
//寻找前一个索引的位置(环形)
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
阈值先不说,和rehash相关,负载因子的概念和hashMap的差不多。
接下来两个函数的作用就是找步长为1的下/上一个索引,这里是环形结构。
记得我们上面说的ThreadLocalMap和HashMap结构不太相同,其实这里就是ThreadLocalMap采取的解决hash冲突的方法,线性探测法。
所谓线性探测法,就是根据初始key值的hashCode确定元素在table中的位置,如果发现该位置已经被其他的元素占用了,则利用固定的算法找到一定步长(这里的步长为1)的下一个位置,以此类推,直至找到正确的位置。
线性探测法其实很简单,就不断找位置而已,但也由此可见,该ThreadLocalMap采用线性探测的办法来解决hash冲突的效率是极低的,所以我们不宜用ThreadLocal来存储过多的变量值。
4.ThreadLocalMap api源码剖析
4.1 构造函数
//ThreadLocalMap是惰性创建的 只有放入第一个元素时ThreadLocalMap才会被创建
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个初始大小为16的Table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//找到该元素在table中的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
//元素个数设置为1
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
该构造函数在createMap中被调用,createMap这函数在ThreadLocal的set()和get()中都出现过。
4.2.关于ThreadLocal的hashCode生成
//当前ThreadLocal的hashCode
private final int threadLocalHashCode = nextHashCode();
//原子类 用于生产当前ThreadLocal
private static AtomicInteger nextHashCode = new AtomicInteger();
//下一个ThreadLocal的hashCode=nextHashCode+HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
每个ThreadLocal对象都有一个threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小为1640531527。这里0x61c88647即为1640531527,选这个数作为步长对于hash散列是有一定好处的,具体我们就不深究了。
4.3.getEntry函数
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
//获取当前元素在table中的位置
int i = key.threadLocalHashCode & (table.length - 1);
ThreadLocal.ThreadLocalMap.Entry e = table[i];
//如果当前位置不为空且命中则直接返回当前结果
if (e != null && e.get() == key)
return e;
else
//否则根据线性探测继续找下一个
return getEntryAfterMiss(key, i, e);
}
//如果getEntry没有找到则调用该方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//基于线性探测的方法往后不断查找直至遇到空的Entry
while (e != null) {
ThreadLocal<?> k = e.get();
//命中返回
if (k == key)
return e;
//如果为空,说明该Entry已经失效,失效的话就进行expungeStaleEntry处理
if (k == null)
expungeStaleEntry(i);
else
//找下一个索引
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
以上就是getEntry的过程,具体流程如下:
- 获取当前ThreadLocal的hashCode,通过hashCode&(len-1)取元素在table中的位置
- 若命中,则直接返回,若e!=null&&e.get()==null,说明该Entry已经失效,我们就需要对其进行清理。
- 否则,找下一个索引的位置,直至遇到空的Entry。
我们来看一下expungeStaleEntry(i) 这个函数的具体作用:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将value设置为null
tab[staleSlot].value = null;
//将当前Entry设置为null help GC
tab[staleSlot] = null;
//Map中元素的数量-1
size--;
//从staleSlot的下一个索引开始遍历
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//若为无效Entry 则按照上面清除无效Entry方式处理
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//若为有效Entry
//取当前Entry在table中的位置
int h = k.threadLocalHashCode & (len - 1);
//若该Entry没有发生过hash冲突,则不用重新设置它的位置
//若发送hash冲突 即h!=i 说明发生过线性检测
if (h != i) {
//设置当前Entry为null
tab[i] = null
//从h开始,线性查找第一个为空的Entry 设置为当前Entry的新位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//返回staleSlot之后第一个为空的Entry的位置
return i;
}
expungeStaleEntry(i)为清除无效Entry的核心函数,有必要对其流程进行认真地分析。
- 设置当前无效的Entry.value为null,这样就不会导致无意识的对象保留(这一点在Effective Java将stack的时候讲过,想了解的话可以自己去查阅),设置Entry为null,帮助GC回收无用的内存,将Map中存储元素的个数减少1.
- 从staleSlot的下一个索引开始遍历,若当前Entry为无效Entry的话,那么就执行上面清除无效Entry的方法。若当前Entry是有效的,那么就判断它是否发生过hash冲突,若h=hashCode&(len-1),且h==i,那么说明该位置上没有发生过hash冲突,就不需要重新设置当前Entry的位置,否则,说明进行过线性检测,那么就从h的位置开始,重新查找当前Entry在table中的位置。(举个例子: 如果当前Entry的h为5,而i却为10,说明5这个位置之前已经被其他元素占用,进行线性检测查找后确定了Entry的位置为10。那么就通过遍历的方法,从5开始重新查找新的位置,这个位置可能的值为5,6,7,8,9,10)。这样子做的目的是为了防止我们清理无效Entry的时候,这个Entry可能(hashCode&(len-1))和Index(线性检测到Entry为空的下标)之间,那么我们查找的时候就会导致我们找到空的Entry就停止,从而找不到后面真正的Entry。
- 直至遇到空的Entry,退出循环,返回stateSlot之后一个为空的Entry的位置。
以上就是expungeStaleEntry(i)的流程,至于第二步要仔细想想为啥要通过线性探测重新定位有效Entry的值。
4.4.set方法
接下来就是分析一下set方法的源码:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//通过线性检测法进行遍历
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//命中 设置新的值 直接返回
if (k == key) {
e.value = value;
return;
}
//若为无效的值 我们需要对其进行替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//若没有对应的Entry,则新建一个Entry
tab[i] = new Entry(key, value);
int sz = ++size;
//如果进行启发式地清理没有发现无效的Entry且当前的数量大于阈值,则进行reHash操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set方法的流程就是:
- 进行线性检测,若发现目标key,则设置新值后直接返回,若发现无效的Entry的话,则进行替换。
- 如果没有发现目标key,也没有无效的Entry,则直接新建一个Entry,并且Map存储元素的个数加一,并且会进行一次启发性的清理,若这次启发性清理函数没有清理任何无效的Entry且当前包含元素的个数超过阈值,那么我们就进行一次rehash()操作。
我们来看一下replaceStaleEntry方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//当前无效Entry的位置
int slotToExpunge = staleSlot;
//从staleSolt的上一个索引通过线性检测进行遍历 直至遇到空的Entry
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//如果找到失效的值 则设置slotToExpunge为当前失效Entry的位置下标
if (e.get() == null)
slotToExpunge = i;
//从staleSolt的下一个所有通过线性检测进行遍历 直至遇到空的Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果找到我们需要的目标key
if (k == key) {
e.value = value;
//与失效Entry进行交换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果staleSlot向前进行遍历找不到无效的Entry
//那么就把slotToExpunge设为当前i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//expungeStaleEntry不再多说,就是从slotToExpunge开始进行遍历清除无效的Entry
//而cleanSomeSlots就是做启发性的清理 这段在后面讲
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果当前的Entry无效 且之前的向前扫描没有扫描到无效的Entry
//那么就把slotToExpunge设为当前i
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//若在table中找不到目标key 则将无效Entry的value置为null
tab[staleSlot].value = null;
//把无效的Entry直接变成我们需要设置的Entry
tab[staleSlot] = new Entry(key, value);
//在探测过程中若发现任何无效的Entry 对其进行清理(连续性清理+启发式清理)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
我们将一下replaceStaleEntry的流程:
- 先从staleSolt的前一个索引开始遍历,直至遇到空的Entry,若发现无效的Entry的,则更新slotToExpunge的值。
- 从staleSolt的下一个索引开始遍历,若找到目标key,则将之前set方法检测到的无效的Entry和存储目标key的Entry的进行替换,若上个步骤没有找到无效的Entry,那么就更新slotToExpunge的值为当前的位置i(已经替换成无效的Entry了),并且进行一次连续性的清理(expungeStaleEntry)加上启发性的清理(cleanSomeSlots),直接返回。若发现无效的Entry,且上个步骤没找到无效的Entry,则更新slotToExpunge的值为当前位置的i,直至遇到空的Entry,退出循环。
- 若没有找到目标key,说明目标key已经失效,那么就直接在之前set方法检测到的无效的Entry的位置上新建一个我们需要设置的Entry。
- 若在探索过程中发现任何无效的Entry,就进行一次连续性的清理(expungeStaleEntry)加上启发性的清理(cleanSomeSlots)。
我们来看一下cleanSomeSlots这个方法:
private boolean cleanSomeSlots(int i, int n) {
//n为当前Map的容量大小 用来控制下面循环的次数
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do
//因为i对应的Entry非无效Entry 所以我们从下一个索引开始
i = nextIndex(i, len);
Entry e = tab[i];
//若该Entry无效
if (e != null && e.get() == null) {
//扩大扫描因子
n = len;
removed = true;
//进行连续性清理
//且返回值为i之后第一个为空的Entry的位置下标
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
//若log(n)次后没有发现无效的Entry 则退出循环
return removed;
}
上面这个函数为启发性地清理无效的Entry,主要两个地方运用到,1、set()的时候有用到该函数,2、进行无效Entry替换的时候有用到该函数,且传入i的值都为非无效Entry的下标(可能为空,可能为有效的Entry),这就是上面代码为什么从i=nextIndex(i, len)开始。
该函数所做的工作就是发现无效的Entry,并进行连续性清理(expungeStaleEntry),若循环log(n)次之后都没发现无效的Entry的话,则退出循环。
最后我们看一下rehash这个函数的源码:
private void rehash() {
//对整个table进行一次全量清理
expungeStaleEntries();
//如果容器存储的个数超过阈值的3/4的话,就进行一次扩容
if (size >= threshold - threshold / 4)
resize();
}
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
//遍历table,对每一个无效的Entry都进行一次连续性清理
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
//扩容函数 其实就是容量扩大2倍
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
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();
if (k == null) {
e.value = null; // Help the GC
} 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;
}
以上就是set()调用到的相关函数,其实我们只要知道set()的时候会顺带清理无效的Entry即可,而且在清理无效的
Entry的同时,若存储的元素数量超过阈值的3/4,就会进行扩容操作。
4.5.remove()
最后,就是ThreadLocalMap的最后一个方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//设置referent为null
e.clear();
//进行一次连续性清理
expungeStaleEntry(i);
return;
}
}
}
remove方法其实很简单,设置弱引用reference为null,再进行一次连续性清除。
有没有发现,ThreadLocalMap的getEntry()、set()、remove()都可以清除无效的Entry.
即设置Entry.value=null,Entry=null,而Entry.reference,前两个方法是被gc自动回收的,remove是手动设置为null的。
5. 关于ThreadLocal内存泄露
思考中…(待写…一方面是线程池,一方面是自己的清理机制)
参考: ThreadLocal源码解读