文章目录
1、ThreadLocalMap 简介
什么是弱引用?
A a = new A(); // 强引用
WeakReference weakA = new WeakReference(a); // 弱引用
a=null;
a是强引用,因为是强引用,后面那个new的A对象不会被GC 回收,但当a==null时,new的对象不会因为有weakA 而保留,依然会被GC回收,因为weakA 是一个弱引用
2、源码分析
下面详细讲解一下 ThreadLocalMap 的源码
成员属性
ThreadLocalMap 内部类的属性
/** 什么是弱引用?
* A a = new A(); 强引用
* WeakReference weakA = new WeakReference(a); 弱引用
*
* a = null
* 下次GC时 对象 a 就被回收了,
*
* key使用的是弱引用保留,key保留的是ThreadLocal对象
* value 使用的是强引用,value保存的是ThreadLocal对象与当前线程相关联的value
*
* entry的 key使用弱引用有什么好处?
* 当ThreadLocal对象失去强引用且对象GC 回收后,散列表中的 与ThreadLocal对象相关联的entry#key再次去key.get() 时,拿到的是null
* 站在map角度就可以区分出哪些entry是过期的,哪些entry是非过期的。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始化当前map 内部散列表的初始长度
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* ThreadLocalMap 内部散列表数组引用,数组的长度 必须是2的次方
*/
private Entry[] table;
/** 当前散列表数组 占用情况,存放了多少entry
*/
private int size = 0;
/**
* 扩容触发阈值 初始值:len * 2 / 3
* 触发后调用rehash() 方法
* rehash() 方法先做一个全局检查 过期数据,把散列表中所有过期的entry移除
* 如果移除以后 当前 散列表中的entry数量任然到达 threshold * 3/4 就进行扩容
*/
private int threshold; // Default to 0
一些小方法
分别是setThreshold ,nextIndex ,prevIndex ,见名知意
这些方法并不是主要的方法,而是主要方法中调用的一些小的操作方法,比较简单,所以就放在一起看。
/**
* 将阈值设置为当前数组长度的 2/3;
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 参数1:当前下标 参数2:当前散列表数组长度
*/
private static int nextIndex(int i, int len) {
//当前下标+1 小于 散列表数组的话,返回len+1
//否则 下标+1 == 散列表数组的长度,返回0;
//实际形成一个环绕式的访问
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 参数1:当前下标 参数2:当前散列表数组长度
*/
private static int prevIndex(int i, int len) {
//当前下标-1 大于等于 0,返回 i-1;
//否则说明 下标-1 等于 -1,返回 散列表最大下标len-1
//实际形成一个环绕式的访问
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
构造方法
实际上有两个构造方法,另一个是私有构造方法:private ThreadLocalMap(ThreadLocalMap parentMap)
/**
* 构造方法:
* 因为 Thread.threadLocals 字段时延迟初始化的,只有线程第一次存储Entry(threadLocal-value) 时,才会创建threadLocalMap对象
*
* firstKey:threadLocal对象
* firstValue:当前线程与threadLocal对象关联的value
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建entry 数组长度为16,表示threadLocalMap内部的散列表
table = new Entry[INITIAL_CAPACITY];
//寻址算法:key.threadLocalHashCode & (table.length - 1)
//table.length 数组的长度一定是2的次方
//table.length-1 有什么特征? 转化为2进制后都是1: 16 - 1 =》 1 0000 - 1 =》 1111
//1111 与任何数组进行 & 运算后 得到的数值 一定是 小于等于 1111
//i 计算出来的结果 一定是 小于等于 1111 的
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建entry对象 存放到指定位置的slot中
table[i] = new Entry(firstKey, firstValue);
//设置size=1
size = 1;
//设置扩容阈值 16 * 2/3 =10
setThreshold(INITIAL_CAPACITY);
}
getEntry方法
这个方法是 ThreadLocal.get()的主要实现方法,get的逻辑就是来源这个方法
/**
* @param key the thread local object
* @return the entry associated with key, or null if no such
*
* ThreadLocal对象 get() 操作实际上是有ThreadLocalMap.getEntry()代理完成的
* key:某个ThreadLocal对象,因为散列表中存储的entry.key 类型四 ThreadLocal
*/
private Entry getEntry(ThreadLocal<?> key) {
//路由规则:ThreadLocal.threadLocalHashCode & (table.length - 1)=> index
int i = key.threadLocalHashCode & (table.length - 1);
//访问散列表中指定位置的slot
Entry e = table[i];
//条件一:成立,说明slot有值
//条件二:成立,说明entry&key 与当前查询的key一致,返回当前entry 给上层就可以了
//key是弱引用
if (e != null && e.get() == key)
return e;
else
//有几种情况到这里?
//1. e==null
//2.e.key != key
//getEntryAfterMiss 方法 会继续向当前桶位后面继续搜索 e.key == key 的entry
//因为存储时 发生hash冲突,并没有在entry层面形成链表,存储时的处理 就是线性的向后找到一个可以使用的slot,并存放进去
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss方法
/**
* @param key the thread local object ThreadLocal对象 就是 key
* @param i the table index for key's hash code key算出来的index
* @param e the entry at table[i] table[index] 中的Entry
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//获取当前ThreadLocalMap 中的散列表table
Entry[] tab = table;
//获取table长度
int len = tab.length;
//条件:e!=null 说明向后查找的范围是有限的,碰到slot==null 的情况,搜索结束
//e:循环处理的当前元素
while (e != null) {
//获取当前slot中entry对象的key
ThreadLocal<?> k = e.get();
//条件成立:说明向后查询过程中找到合适的entry了,返回entry就ok了
if (k == key)
return e;
//条件成立:说明当前slot中的entry#key 关联的 ThreadLocal对象已经被GC回收了,因为key是弱引用,e.get() == null
if (k == null)
//做一次探测式过期数据回收
expungeStaleEntry(i);
else
//更新index,继续向后搜索
i = nextIndex(i, len);
//获取下一个slot中的entry
e = tab[i];
}
//执行到这里说明:关联区段内 都没有找到相关数据
return null;
}
expungeStaleEntry方法
探测式清理过期数据:向后查找过期数据,碰到slot==null就返回它的下标
/**
* A a = new A()
* WeakReference b = new WeakReference(a)
* a=null
* 此时 b也等于null
*
* 参数stateSlot table[stateSlot] 就是一个过期数据,以这个位置开始 继续向后查找过期数据,知道碰到slot==null的情况结束
*/
private int expungeStaleEntry(int staleSlot) {
//获取散列表
Entry[] tab = table;
//获取散列表当前长度
int len = tab.length;
// expunge entry at staleSlot
//help GC
tab[staleSlot].value = null;
//因为staleSlot位置的entry 是过去的 这里直接置为null
tab[staleSlot] = null;
//因为上面干掉一个元素,所以 -1;
size--;
// Rehash until we encounter null
//e:表示当前遍历节点
Entry e;
//i:表示当前遍历的index
int i;
//for 循环从staleSlot+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;
//因为上面干掉一个元素,所以 -1;
size--;
} else {
//执行到这里,说明当前遍历的slot中对应的entry是非过期数据
//因为前面有可能清理掉了几个过期数据
//且当前entry存储时有可能碰到hash冲突,往后偏移存储了,这个时候 应该去优化位置,让这个位置更可靠近 正确位置
//这样的话,查询的时候 效率才会更高
//重新计算当前entry 对应的index
int h = k.threadLocalHashCode & (len - 1);
//条件成立:说明当前entry存储时 就是发生hash冲突,然后向后偏移过了
if (h != i) {
//将entry 当前位置 设置为 null
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
//h 是正确位置
//以正确位置h 开始,向后查找第一个可以存放entry的位置
while (tab[h] != null)
h = nextIndex(h, len);
//将当前元素放入到 举例正确位置更近的位置(有可能就是正确位置)
tab[h] = e;
}
}
}
return i;
}
set方法
ThreadLocal.set() 方法的 主要实现就是通过ThreadLocalMap.set()来实现的。
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*
* ThreadLocal 使用set方法 给当前线程添加 ThreadLocal-value 键值对
*/
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;
//计算当前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)]) {
//获取当前元素key
ThreadLocal<?> k = e.get();
//条件成立:说明当前set操作时替换操作
if (k == key) {
e.value = value;
return;
}
//条件成立:说明向下寻找过程中 碰到entry#key == null 的情况了,说明当前entry 是过期数据
if (k == null) {
//2. 碰到一个过期的 slot,这个时候就可以强行占用
//替换逻辑
replaceStaleEntry(key, value, i);
return;
}
}
//执行到这里,说明for循环碰到了 slot==null的情况
//在合适的slot中创建新的entry元素
tab[i] = new Entry(key, value);
//因为是新添加,size++
int sz = ++size;
//做一次启发式清理
//条件一:!cleanSomeSlots(i, sz)成立:说明启发式清理工作,未清理到任何数据
//条件二:sz >= threshold 成立:说明当前table内的entry已经达到了扩容阈值了,会触发rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry方法
在set()方法向下寻找可用桶位的过程中,如果碰到Entry.key == null (弱引用) 的情况,说明当前entry是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑:
/**
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*
* 替换过期的slot
*
* key:键 ThreadLocal对象
* value:val
* staleSlot:上层方法 set方法,迭代查找时,发现的当前这个slot是一个过期的entry
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
//获取散列表
Entry[] tab = table;
//获取散列表长度
int len = tab.length;
//临时变量
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
//表示 开始探测式清理过期数据的 开始下标,默认从当前stateSlot开始
int slotToExpunge = staleSlot;
//以当前stateSlot开始 向前迭代查找,默认从当前 stateSlot开始,for循环一致碰到null结束
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//条件成立:说明向前找到了过期数据,更新 探测清理过期数据的开始下标为 i
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
//以当前 stateSlot 向后去查找,知道碰到null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取当前元素key
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
//条件成立:说明咱们是一个替换逻辑
if (k == key) {
//替换新数据
e.value = value;
//交换位置的逻辑
//将 table[stateSlot] 这个过期数据放到 当前循环到的table[i] 这个位置
tab[i] = tab[staleSlot];
//将tab[stateSlot] 中保存为当前entry,这样的话,咱们这个数据位置就被优化了
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//条件成立:
// 1.说明 replaceStaleEntry一开始时的向前查找过期数据 并未找到过期的entry
// 2.向后检查过程中也未发现过期数据(向后查询是下面那个if做个事情)
if (slotToExpunge == staleSlot)
//开始探测式清理过期 数据的下标 修改为 当前循环的index
slotToExpunge = i;
//cleanSomeSlots:启发式清理
//参数: 探测式 结束的位置(entry==null的位置)数组长度
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
//条件1: k==null 成立:说明当前遍历的entry 是一个过期数据..
//条件2:slotToExpunge == staleSlot成立:一开始时的向前查找过期数据 并未找到过期的entry
if (k == null && slotToExpunge == staleSlot)
//因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为当前位置
//前提条件:前驱扫描时 未发现过期数据
slotToExpunge = i;
}
//什么时候执行到这里?
//向后查找过程中 并未发现 k==key 的entry,说明当前set操作 是一个添加逻辑
// If key not found, put new entry in stale slot
//直接将新数据添加到 tab[staleSlot] 对应的slot中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//条件成立:除了当前staleSlot以外,还未发现其它的过期slot了,所以要开启清理数据的逻辑
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
cleanSomeSlots方法
启发式清理过期节点:
启发式清理工作的开始位置就是 :探测式 结束的位置(entry==null的位置)
/**
* 参数1:启发式清理工作的开始位置,探测式 结束的位置(entry==null的位置)
* 参数2:一般是数组长度table.length,这里n表示结束条件
*/
private boolean cleanSomeSlots(int i, int n) {
//表示启发式清理工作 是否清理过过期数据
boolean removed = false;
Entry[] tab = table;
//散列表长度
int len = tab.length;
do {
//为什么这里是从i+1 开始呢?
//cleanSomeSlots(i = expungeStaleEntry(slotToExpunge), len);
//因为 i = expungeStaleEntry(slotToExpunge) 返回值一定是null的下标
//获取当前i 的下一个下标
i = nextIndex(i, len);
Entry e = tab[i];
//条件一:e != null成立
//条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据
if (e != null && e.get() == null) {
//重新更新n 为table数组长度
n = len;
//表示清理过数据
removed = true;
//以当前过期的slot 为开始节点 做一次 探测式清理工作
i = expungeStaleEntry(i);
}
//探测5次
//假设table 长度为16
//16 >>> 1 ==> 8
//8 >>> 1 ==> 4
//4 >>> 1 ==> 2
//2 >>> 1 ==> 1
//1 >>> 1 ==> 0
} while ( (n >>>= 1) != 0);
return removed;
}
rehash方法
private void rehash() {
//这个方法执行完后,所有散列表内的所有过期的数据都会被干掉
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//条件成立:说明清理完过期数据后,当前散列表内的entry数量 任然达到了 阈值的3/4,真正触发扩容
if (size >= threshold - threshold / 4)
//真正扩容
resize();
}
}
resize方法
真正扩容的方法:
private void resize() {
//获取当前散列表
Entry[] oldTab = table;
//获取当前散列表长度
int oldLen = oldTab.length;
//计算新表长度
int newLen = oldLen * 2;
//创建一个新的table散列表
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#key
ThreadLocal<?> k = e.get();
//条件成立:说明老表中的当前位置的entry是一个过期数据,help GC,不用 在迁移
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;
//将扩容后的新表的引用保存到 threadLocalMap 对象的table这里
table = newTab;
}
remove方法
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) {
e.clear();
//探测式清理
expungeStaleEntry(i);
return;
}
}
}