ThreadLocal源码分析_02内核(ThreadLocalMap)
本篇主要介绍ThreadLocal的内核ThreadLocalMap(ThreadLocal的静态内部类),在学习ThreadLocalMap内核之前,再来复习一下ThreadLocal的执行流程:
下面就正式分析一下ThreadLocalMap的原理:
1、成员属性
/**
* 初始化当前map内部散列表数组的初始长度
*/
private static final int INITIAL_CAPACITY = 16;
/**
* ThreadlocalMap内部散列表数组的引用,数组的长度必须是2的次方数
*/
private Entry[] table;
/**
* 当前散列表数组占用情况,存放多少个entry
*/
private int size = 0;
/**
* 扩容出发阙值,初始值为:len*2/3
* 触发后调用reshash()方法
* rehash()方法先做一次全量检查全局过期数据,把散列表中所有过期的entry全部移除
* 如果移除之后,当前散列表中的entry个数仍然达到(threshold-threshold/4)
* 即当前threshold阙值的3/4就进行扩容
*/
private int threshold; // Default to 0
/**
* 将扩容阙值设置为散列表当前数组长度的2倍/3
* @param len
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
2、内部类
这里内部类Entry继承了弱引用WeakReference:
-
什么是弱引用呢?eg:
A a = new A(); // 强引用 WeakReference weakA = new WeakReference(a); // 弱引用
当a=null的时候,则下一次GC的时候对象a就倍回收了,不管有没有弱引用在关联这个对象:
-
key使用的是弱引用保留,key保存的是threadLocal对象。
-
value使用的是强引用,value保存的是threadLocal对象与当前线程相关联的value
/**
* ThreadLocalMap的Entry内部类,成员属性有两个,key(threadLocal对象)
* 和value(threadlocal对象与当前线程相关联的value)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
//value:使用的是强引用,value保存的是threadLocal对象与当前线程相关联的value
Object value;
//k:key 使用的是弱引用,key保存的是threadLocal对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry的key这样设计有什么好处呢?
- 当threadLocal对象失去强引用且对象倍GC回收之后,散列表中的与threadLocal对象相关联的entry#key再次失去,key.get()时,拿到的是null
- 站在map的角度就可以区分出entry哪些是过期的,哪些是entry是非过期的。
3、构造方法
- Thread.threadLocals字段是延迟初始化的,只有线程第一次存储threadLocal-value的时候才会创建。
/**
*
* @param firstKey threadLocal对象
* @param firstValue 当前线程与ThreadLocal相关联的value
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建entry数组长度为18,表示threadLocalMap内部的散列表
table = new Entry[INITIAL_CAPACITY];
//寻址算法:key.threadLocalHashCode & (table.length-1),确定要存储的下标值
//table的长度一定是2的次方数
//2的次方数-1有什么特征呢?转化为2进制后都是1 16 10000 => 1111
//1111在与任何数值进行&运算后,得到的数值一定是<=1111
//i计算出来的结果一定是<=1111
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建entry对象存放到指定位置的slot中
table[i] = new Entry(firstKey, firstValue);
//设置当前数组中的元素数量为1
size = 1;
//设置扩容阙值 (当前数组的长度*2/3)
setThreshold(INITIAL_CAPACITY);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
4、成员方法
nextIndex方法
/**
* 获得下一个数组下标的位置
* @param i 当前散列表数组下标
* @param len 当前散列表数组长度
* @return
*/
private static int nextIndex(int i, int len) {
//当前数组下标i+1,如果计算结果:小于散列表的长度,则返回+1后的值
//否则:这种情况就是下标i+1=len 返回0
return ((i + 1 < len) ? i + 1 : 0);
//实际上形成一个环绕式的访问,如下图:
}
**prevIndex方法 **
/**
* 获得上一个数组下标的位置
* @param i 当前数组下标
* @param len 当前散列表数组的长度
* @return
*/
private static int prevIndex(int i, int len) {
//当前下标i-1大于等于0 则返回i-1后的值
//否则,说明当前下标i-1== -1,此时返回散列表的最大下标
//实际上形成一个环绕式的访问
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
getEntry()方法
- ThreadLocal对象的get()方法实际上是由ThreadLocalMap.getEntry()代理完成的。
/**
* 根据Threadlocal对象,获取对应的Entry对象
* key:某个threadLocal对象,因为散列表中存储的entry.key类型是ThreadLocal
* @param key
* @return Entry
*/
private Entry getEntry(ThreadLocal<?> key) {
//桶位路由规则:ThreadLocal.threadLocalHashCode & (table.length - 1) ==> index
int i = key.threadLocalHashCode & (table.length - 1);
//访问散列表中指定位置的solt桶,拿到Entry
Entry e = table[i];
//条件1:成立 说明slot有值
//条件2:成立 说明entry#key 与当前查询的key一致,返回当前entry交给上层就可以了
if (e != null && e.get() == key)
return e;
else
//有几种情况会执行到这里?
//情况1:桶位上的(Entry)e==null
//情况2:根据key没有查询到 说明e.key!=key
//getEntryAfterMiss方法:会继续向当前桶位向后继续搜索,e.key=key的entry
//为什么这样做呢?
//因为存储的时候如果发生hash冲突后,并没有在entry面形成链表的结构
//那么,当存储的时候,如果hash冲突了,处理的方式就是线性的向后找到一个可以用的slot
//并将entry对象存放进去
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss方法
- 继续在桶位之后的桶中寻找e.key==key条件的桶!
/**
* 继续在桶位i之后寻找满足条件e.key==key的桶位
* @param key threadLocal对象表示 key
* @param i 表示计算出来的桶位
* @param e 表示table[i]中的entry 用以做返回值的
* @return
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//获取当前threadLocalMap中的散列表对象table
Entry[] tab = table;
//获取table的长度
int len = tab.length;
//while循环条件:e!=null 说明向后查找的范围是有限的 碰到slot==null的情况,则搜索结束
//e:循环处理的当前元素
while (e != null) {
//获取当前slot中entry对象的key
ThreadLocal<?> k = e.get();
//k==key条件成立:说明向后查询的过程中找到合适的entry了,直接返回entry就ok了
if (k == key)
//找到的情况下,就会这里返回entry对象了
return e;
//k==null条件成立:说明当前slot中的entry#key 关联的ThreadLocal对象已经倍GC回收了
//因为key是弱引用 key=e.get()==null
if (k == null)
//做一次探测式过期数据回收
expungeStaleEntry(i);
else
//更新index,继续向后搜索
i = nextIndex(i, len);
//获取下一个slot中的entry
e = tab[i];
}
//执行到这里,说明关联区段内都没找到相应数据
return null;
}
expungeStaleEntry方法
- 探测式清理过期数据:以stateSlot位置开始向后查找过期数据,直到碰到slot==null的情况结束,并返回结束时数组下标位置。
/**
*
* @param staleSlot table[staleSlot]就是一个过期数据
* 以这个位置开始继续向后查找过期数据,直到碰到slot==null的情况结束
* @return
*/
private int expungeStaleEntry(int staleSlot) {
//获取当前散列表对象
Entry[] tab = table;
//获取当前散列表的长度
int len = tab.length;
//先将当前的slot过期数据清空
tab[staleSlot].value = null;
//因为当前stateSlot位置的entry是过期的,所以直接置为null
tab[staleSlot] = null;
//因为上面干掉了一个元素,所以size--
size--;
//e:表示当前遍历节点
Entry e;
//i:表示当前遍历的index
int i;
//for循环从stateSlot+1的位置开始搜索过期数据,直到碰到slot==null为止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//进入到for循环里面,当前entry不一定为null
//获取当前遍历节点 entry的key
ThreadLocal<?> k = e.get();
//条件成立:说明 k表示的threadLocal对象已经被GC回收了
//则,当前的entry属于脏数据了
if (k == null) {
//help gc
e.value = null;
//脏数据对应的slot置为null
tab[i] = null;
//因为上面干掉了一个元素,所以size--
size--;
} else {
//执行到这里,说明当前遍历的slot中对应的slot中对应的entry是非过期数据
//因为前面有可能清理掉了几个过期数据,且当前entry存储时有可能hash冲突了
//应该向后偏移,这个时候应该去优化位置,让这个位置更加靠近正确位置!
//重新计算当前entry对应的index
int h = k.threadLocalHashCode & (len - 1);
//条件成立,说明当前entry存储的时候,就是发生过hash冲突,然后向后偏移了
if (h != i) {
//将当前entry设置为null
tab[i] = null;
//这时 h是正确位置
//以正确位置h开始,开始向后查找第一个可以存放entry的位置
while (tab[h] != null)
h = nextIndex(h, len);
//将当前元素放入到距离正确位置更近的位置(有可能就是正确位置)
tab[h] = e;
}
}
}
return i;
}
该方法的执行流程图如下:
set方法 :
- ThreadLocal使用set方法给当前线程添加ThreadLocal-value键值对:
/**
* ThreadLocal使用set方法给当前线程添加threadLocal-value值
* @param key threadlocal对象(Entry的key值
* @param value Entry的value值
*/
private void set(ThreadLocal<?> key, Object value) {
//获取散列表
Entry[] tab = table;
//获取散列表的长度
int len = tab.length;
//计算当前key 在散列表中的对应位置
int i = key.threadLocalHashCode & (len-1);
//以当前key对应的slot向后查询,直到找到可以用的slot桶位
//什么时候slot桶位可以使用呢?
//1.k==key 说明是替换
//2.碰到一个过期的slot,这个时候可以强行占用
//3.查找过程中碰到slot==null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取当前entry的ThreadLocal对象
ThreadLocal<?> k = e.get();
//条件成立:说明当前的set操作是一个替换操作
if (k == key) {
//做替换逻辑
e.value = value;
return;
}
//条件成立:说明向下寻找的过程中碰到entry#key==null的情况了,说明当前entry是过期数据
if (k == null) {
//碰到一个过期的slot,可以强行占用该桶位
//替换过期的数据
replaceStaleEntry(key, value, i);
return;
}
}
//执行到这里,说明for循环碰到了slot==null的情况
//在合适的slot中,创建新的entry对象
tab[i] = new Entry(key, value);
//因为是新添加,所以++size
int sz = ++size;
//做一次启发式的清理:
//条件1:!cleanSomeSlots(i, sz)成立:说明启发式清理工作未清理到任何数据
//条件2:sz >= threshold成立 说明当前table内的entry已经达到了扩容阙值..会触发rehash操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry方法
- 在set()方法向下寻找可用slot桶位的过程中,如果碰到key==null的情况,说明当前entry是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑。
/**
* 强行占用该桶位以清理过期数据
* @param key threadLocal对象
* @param value 当前与threadLocal所关联的变量值
* @param staleSlot 上层set方法,迭代的时候发现该下标位置的slot是一个过期的entry
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
//获取散列表
Entry[] tab = table;
//获取散列表数组的长度
int len = tab.length;
//临时变量
Entry e;
//slotToExpunge表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始
int slotToExpunge = staleSlot;
//以当前staleSlot开始,向前迭代查找,找有没有过期的数据,for循环一直到碰到null结束
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//条件成立:说明向前找到了过期数据,更新探测清理过期数据的开始下标为i
if (e.get() == null)
slotToExpunge = i;
//以当前staleSlot的后一个位置向后去查找,直到碰到null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取entry元素的threadLocal对象
ThreadLocal<?> k = e.get();
//条件成立:说明是一个替换逻辑
if (k == key) {
e.value = value;
//交换位置的逻辑...
//将table[staleSlot]这个过期数据放到当前循环的table[i]这个位置
tab[i] = tab[staleSlot];
//将table[staleSlot]中保存为当前entry。这样的话,这个数据位置就被优化了
tab[staleSlot] = e;
//如果条件成立:
//1.说明replaceStaleEntry一开始的时候向前查找过期数据 并未找到过期的entry
//2.向后检查的过程中也未发现过期数据
if (slotToExpunge == staleSlot)
//开始探测式清理过期数据的下标 修改为当前循环的index
slotToExpunge = i;
//cleanSomeSlots:启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//条件1:k==null成立 说明当前遍历的entry是一个过期元素
//条件2:slotToExpunge == staleSlot成立,一开始向前查找过期数据并未找到过期的entry
if (k == null && slotToExpunge == staleSlot)
//因为向后查询的过程中查找到一个过期数据,更新slotToExpunge为当前位置
//前提条件是前驱扫描的时候未发现过期数据
slotToExpunge = i;
}
//什么时候执行到这里呢?
//向后查找的过程中并未发现k==key的entry 说明当前set操作是一个添加逻辑
//直接将新数据添加到table[staleSlot]
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//条件成立:除了当前的staleSlot以外,还发现其他过期的slot了,所以要开始清理过期数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
**cleanSomeSlots方法 **
- 启发式清理工作的方法:
/**
* 启发式清理工作的方法:
* @param i 启发式清理工作开始的位置
* @param n 一般传递的是table的长度 这里n也表示结束条件
* @return
*/
private boolean cleanSomeSlots(int i, int n) {
//表示启发式清理工作,是否清除过过期数据
boolean removed = false;
//获取当前map的散列表引用
Entry[] tab = table;
//获取当前散列表数组的长度
int len = tab.length;
do {
//这里为什么不是从i就检查呢?
//因为cleanSomeSlots(i=expungeStaleEntry(??),n)
//expungeStaleEntry(??)这个位置的值一定是null 所以从i+1开始
i = nextIndex(i, len);
//获取table中当前下标为i的元素
Entry e = tab[i];
//条件1:e!=null 成立说明该slot不为null
//条件2:e.get()==null成立 说明该entry保存的entry是一个过期数据
if (e != null && e.get() == null) {
//重新更新n为table数组长度
n = len;
//表示清理过数据
removed = true;
//探测式清理以当前的slot为开始节点,做一次探测式清理工作
i = expungeStaleEntry(i);
}
//假设table长度为16
//16 >>> 1 ==> 8
//8 >>> 1 ==> 4
//4 >>> 1 ==> 2
//2 >>> 1 ==> 1
//1 >>> 1 ==> 0
} while ( (n >>>= 1) != 0);
return removed;
}
cleanSomeSolts启发式清理流程图:
reshash方法:
- 扩容方法:
/**
* 扩容方法
*/
private void rehash() {
//这个方执行完以后,当前散列表内所有的过期数据,都会被干掉
expungeStaleEntries();
//条件成立:说明清理完过期数据后
//当前散列表内的entry数量仍然达到了threshold*3/4,则真正触发扩容
if (size >= threshold - threshold / 4)
//真正执行扩容过的方法
resize();
}
resize方法:
- 真正执行扩容的方法:
//真正执行扩容的方法
private void resize() {
//获取当前散列表
Entry[] oldTab = table;
//获取当前散列表的长度
int oldLen = oldTab.length;
//计算出扩容后的散列表容量大小,oldLen*2
int newLen = oldLen * 2;
//依据新计算出来的散列表容量创建出一个新的散列表
Entry[] newTab = new Entry[newLen];
//表示新table(散列表)中的entry数量
int count = 0;
//遍历老表,迁移数据到新表
for (int j = 0; j < oldLen; ++j) {
//访问老表的指定位置的slot
Entry e = oldTab[j];
//条件成立:说明老表中的指定位置有数据(注意 此时散列表中是没有过期数据的)
if (e != null) {
//获取当前entry的ThreadLocal对象(即key)
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
//执行到这里,说明老表的当前位置是非过期元素:即正常数据,需要前移到新表中的数据
//计算出当前entry在扩容之后的在新表中的存储位置
int h = k.threadLocalHashCode & (newLen - 1);
//while循环就是拿到一个距离h最近的一个可以使用的slot
while (newTab[h] != null)
h = nextIndex(h, newLen);
//将数据存放到新表合适的slot中
newTab[h] = e;
//数量+1
count++;
}
}
}
//设置下一次触发扩容的指标
setThreshold(newLen);
//设置当前散列表中元素的个数
size = count;
//将扩容后的新表的引用保存到了ThreadLocal对象的table这里
table = newTab;
}
remove方法
- remove方法就比较简单了
/**
* 从threadlocalmap对象的table散列表中移除对应key的entry对象
* @param key
*/
private void remove(ThreadLocal<?> key) {
//获取当前散列表对象
Entry[] tab = table;
//获取当前散列表对象的长度
int len = tab.length;
//计算出当前Threadlocal在散列表中的下标
int i = key.threadLocalHashCode & (len-1);
//由于threadLocalMap对象是采用hash偏移的方式来解决hash冲突的问题,所以循环条件是从
//当前计算的下标开始直到遇到e==null为止
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
//探测式清除过期数据
expungeStaleEntry(i);
return;
}
}
}
table;
//获取当前散列表对象的长度
int len = tab.length;
//计算出当前Threadlocal在散列表中的下标
int i = key.threadLocalHashCode & (len-1);
//由于threadLocalMap对象是采用hash偏移的方式来解决hash冲突的问题,所以循环条件是从
//当前计算的下标开始直到遇到e==null为止
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
//探测式清除过期数据
expungeStaleEntry(i);
return;
}
}
}