在了解ThreadLocalMap之前,先了解下Java的弱引用;
弱引用的定义:
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存.通过使用弱引用来避免内存泄漏.
Product productA = new Product();
WeakReference<Product> weakProductA = new WeakReference<>(productA);
接下来主要看ThreadLocal中的ThreadLocalMap静态方法
ThreadLocalMap中:通过key 使用弱引用,key保存的是ThreadLocal对象;
value: 使用的是强引用,value保存的是ThreadLocal对象与当前线程相关联的value
这样设计的好处是,当ThreadLocal对象失去强引用后且对象GC回收后,散列表中与threadLocal对象关联的entry#key再次去 key.get()时,拿到的是null; 这样可以区分哪些entry是过期的。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
接下来有几个常规属性
INITIAL_CAPACITY:初始化当前threadLocal散列表的 初始长度
private static final int INITIAL_CAPACITY = 16;
table:threadLocalMap 内部散列表数组引用, 数组的长度必须是2的次方数
private Entry[] table;
size:当前散列表数组 占用情况,存放多个entry
private int size = 0;
threshold: 扩容出发阈值 ,当初始值为: len *2/3
private int threshold; // Default to 0
nextIndex()方法: 参数1: 当前下标; 参数2: 当前散列表数组长度
private static int nextIndex(int i, int len) {
// 当前下标+1 小于当前散列表数组的话, +1后的值
// 否则 情况就是 下标+1 == len ,返回0
// 实际形成一个环绕式的访问
return ((i + 1 < len) ? i + 1 : 0);
}
prevIndex()方法:
private static int prevIndex(int i, int len) {
// 当前下标-1 大于等于0 返回 -1后的值就OK
// 否则说明当前下标 -1 == -1 此时 当前返回散列表最大下标。
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap的构造方法:因为Thread.threadLocals字段是延迟初始化的,只有线程第一次存储 threadLocal-value时,才会创建threadLocalMap对象.有两个参数;firstKey:threadLocal对象; firstValue: 当前线程与threadLocal对象关联的value;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个为INITIAL_CAPACITY ,表示threadLocalMap内部的散列表
table = new Entry[INITIAL_CAPACITY];
// 寻址算法: key.threadLocalHashCode & (table.length -1 )
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建一个entry
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置当前的扩容阈值
setThreshold(INITIAL_CAPACITY);
}
getEntry()方法:
private Entry getEntry(ThreadLocal<?> key) {
// 获取当前ThreadLocal的hasCode,ThreadLocal.threadLocalHashCode &(table - 1)
int i = key.threadLocalHashCode & (table.length - 1);
// 访问散列表中 指定位置的slot
// 说明entry #key 与当前查询的key 一致,直接返回上层就行
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 继续向当前桶位后面继续搜索key == e.key 的entry
// 为什么这样做?
// 因为 存储时, 发生hash冲突后, 并没有在entry层面形成 链表
// 存储时的处理 就是线性的向后找到一个可以使用的slot,并且存放进去
return getEntryAfterMiss(key, i, e);
}
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;
// 计算ThreadLocal对象在当前散列表中 对应的位置
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();
// 说明key 一致,是一个替换操作的逻辑
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;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry()方法
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).
// 不碍事就是谈测式 清理过期数据的 开始下标. 默认从当前 staleSlot开始
int slotToExpunge = staleSlot;
// 以当前staleSlot 开始,向前迭代查找, ,找没有过期的数据。一直到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
// 以当前staleSlot向后去查找,直到碰到 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.
// 如果key 与插入的key一致,说明找到了替换逻辑
if (k == key) {
// 替换新旧数据
e.value = value;
// 将table[staleSlot]这个过期数据 放到 当前循环到的 table[i] 这个位置
tab[i] = tab[staleSlot];
// 将tab[staleSlot] 中保存为当前entry. 这样的话,咱们这个数据位置就被优化了
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果成立: 说明方法一开始向前查找过期数据 并未找到 过期的entry
if (slotToExpunge == staleSlot)
// 开始探测清理过期数据的下标,修改为当前循环的 index
slotToExpunge = i;
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.
// 条件一: k== null 说明:当前遍历的entry是一个过期数据
// 条件二: 说明方法一开始向前查找过期数据 并未找到 过期的entry
if (k == null && slotToExpunge == staleSlot)
// 因为向后查询过程中,查找到了一个过期数据,进行更新slotToExpunge 为当前位置
// 前提条件 是 前驱扫描 时 未发现过期数据
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 什么时候执行到这里?
// 向后查找过程中, 并发发现 k == key 的entry,说明当前set操作时一个添加逻辑
//直接将新数据添加到 table[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);
}
expungeStaleEntry()方法
private int expungeStaleEntry(int staleSlot) {
// 获取散列表
Entry[] tab = table;
// 获取散列表当前长度
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
// 因为 staleSlot位置的 entry 是过期的 这里直接置空
tab[staleSlot] = null;
// 上面干掉一个元素, --1;
size--;
// Rehash until we encounter null
// 表示当前遍历的entry
Entry e;
// 表示当前遍历的下标
int i;
// for循环 从 staleSlot +1 的位置开始 搜索过期数据,直到碰到 slot == null 结束
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 进入到
// 获取当前遍历节点 entry的 key
ThreadLocal<?> k = e.get();
// 条件成立: 说明k 表示的 threadLocal对象 已经被GC回收了.......当前entry 属于脏数据了
if (k == null) {
e.value = null;
tab[i] = null;
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;
}
cleanSomeSlots()方法:
private boolean cleanSomeSlots(int i, int n) {
// 表示启发式清理工作 是否清除过期数据
boolean removed = false;
// 获取当前map的散列表引用
Entry[] tab = table;
// 获取当前散列表的长度
int len = tab.length;
do {
// 获取当前i的下一个下标
i = nextIndex(i, len);
// 获取table 中当前下标为i的元素
Entry e = tab[i];
// 条件1: e != null
//条件二: e.get() == null; 说明当前slot中保存的entry 是一个过期的数据
if (e != null && e.get() == null) {
// 重新更新n 为 table数组长度
n = len;
// 表示清理过数据
removed = true;
// 以当前过期的slot 为开始节点
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}