趣链
的面试中被问到ThreadLocal
的相关问题,被问的一脸懵*,因此有次总结.线程局部变量
是我一直对他的叫法,刚开始接触是用来保存jdbc
的链接(这样想一想我接触的还挺早的)ThreadLocal
并非底层的集合类,而是一个工具类,全部的线程私有数据都被保存在各个Thread
对象中一个叫作threadLocals
的ThreadLocalMap
的成员变量里,ThreadLocal
也只是操做这些变量的工具类.Thread
都会存有一个ThreadLocalMap
的对象供多个ThreadLocal
的类调用,因此你能够发现多个ThreadLocal
操做的Map
会是同一个,而当ThreadLocal
做为key
的发生哈希碰撞时,会从当前位置开始向后环型遍历,找到一个空位置,这方法咱们能够称之为线性探测法.ThreadLocalMap
出人意料的并无继承任何一个类或接口,是彻底独立的类。// 默认的初始容量 必定要是二的次幂
private static final int INITIAL_CAPACITY = 16;
// 元素数组/条目数组
private Entry[] table;
// 大小,用于记录数组中实际存在的Entry数目
private int size = 0;
// 阈值
private int threshold; // Default to 0 构造方法
复制代码
// 默认访问权限的初始化方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 使用默认的`容量`初始化数组
table = new Entry[INITIAL_CAPACITY];
// 以`ThreadLocal`的`HashCode`计算下标
// 这里和HashMap中的计算方式同样,都用与运算
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 赋值 修改大小并计算阈值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// `setThreshold`方法也特别简单,就是2/3的容量。
setThreshold(INITIAL_CAPACITY);
}
复制代码
以ThreadLocal
为Key
获取对应的Entry
。java
由于ThreadLocalMap
底层也是使用数组做为数据结构,因此该方法也借鉴了HashMap
中求元素下标的方式.面试
在获取的元素为空的时候还会调用getEntryAfterMiss
作后续处理.数组
private Entry getEntry(ThreadLocal<?> key) {
// 和HashMap中同样的下标计算方式
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 获取到对应的Entry以后就分两步
if (e != null && e.get() == key)
// 1. e不为空且threadLocal相等
return e;
else
// 2. e为空或者threadLocal不相等
return getEntryAfterMiss(key, i, e);
}
复制代码
Hash
计算下标后,没获取到对应的Entry
对象的时候调用。key
表示的Entry
对象。private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 此时注意若是从上面状况`2.`进来时,
// e为空则直接返回null,不会进入while循环
// 只有e不为空且e.get() != key时才会进while循环
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到相同的k,返回获得的Entry,get操做结束
if (k == key)
return e;
// 若此时的k为空,那么e则被标记为`Stale`须要被`expunge`
if (k == null)
expungeStaleEntry(i);
else // 下面两个都是遍历的相关操做
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
复制代码
staleSlot
位置的Entry对象,而且会清理当前节点到下一个null
节点中间的过时Enyru
.内存泄漏
威胁的主力方法,在整个ThreadLocalMap
中会屡次调用./** * 清空旧的Entry对象 * @param staleSlot: 清理的起始位置 * @param return: 返回的是第一个为空的Entry下标 */
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清空`staleSlot`位置的Entry
// value引用置为空以后,对象被标记为不可达,下次GC就会被回收.
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 经过nextIndex从`staleSlot`的下一个开始向后遍历Entry数组,直到e不为空
// e赋值为当前的Entry对象
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 当k为空的时候清空节点信息
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else { // 如下为k存在的状况
int h = k.threadLocalHashCode & (len - 1);
// 元素下标和key计算的不同,代表是出现`Hash碰撞`以后调整的位置
// 将当前的元素移动到下一个null位置
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
复制代码
ThreadLocalMap
底层结构和HashMap
同样也是数组,也是经过hash
肯定下标,也同样会发生Hash碰撞
,咱们知道在HashMap
中为了解决Hash碰撞
的问题选择了拉链法,但对于ThreadLocalMap
并无那么高的复杂度,因此此处选择的是开放地址法
.Entry
再肯定数组位置以后直接就开始了遍历,若是key
不匹配就日后遍历找到key
匹配的元素覆盖,或者key == null
的替换.private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 整个循环的功能就是找到相同的key覆盖value
// 或者找到key为null的节点覆盖节点信息
// 只有在e==null的时候跳出循环执行下面的代码
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到相等的k,则直接替换value,set操做结束
if (k == key) {
e.value = value;
return;
}
// k为空表示该节点过时,直接替换该节点
if (k == null) { // 1.
replaceStaleEntry(key, value, i);
return;
}
}
// 走到这一步就是找到了e为空的位置,否则在上面两个判断里都return了
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
复制代码
1.
处进入该方法,用于替换key
为空的Entry
节点,顺带清除数组中的过时节点./** * 从`set.1.`处进入,key是插入元素ThreadLocal的hash,staleSlot为key为空的数组节点下标 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
// 从传入位置,即插入时发现k为null的位置开始,向前遍历,直到数组元素为空
// 找到最前面一个key为null的值.
// 这里要吐槽一下源代码...大括号都不加 习惯真差
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
if (e.get() == null)
// 由于是环状遍历因此此时slotToExpunge是可能等于staleSlot的
slotToExpunge = i;
}
// 该段循环的功能就是向后遍历找到`key`相等的节点并替换
// 并对以后的元素进行清理
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// e就是tab[i],因此下三行代码的功能就是替换Entry
// 新的Entry实际仍是在staleSlot下标的位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 由于接下来要进行清理操做,因此此处须要从新肯定清理的起点.
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 其实我对这个`slotToExpunge == staleSlot`的判断一直挺疑惑的,为何须要这个判断?
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// e==null时跳到下面代码运行
// 清空并从新赋值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// set后的清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
Entry
i
向后开始遍历log2(n)
次,若是之间发现过时Entry
会直接将n
扩充到len
能够说全数组范围的遍历.发现过时Entry
就调用expungeStaleEntry
清除直到未发现Entry
为止./** * @param i 清除的起始节点位置 * @param n 遍历控制,每次扫描都是log2(n)次,通常取当前数组的`size`或`len` */
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`时,n变为len
// 即扩大范围,全数组范围在遍历一次
n = len;
removed = true;
i = expungeStaleEntry(i);
}
// 无符号右移一位至关于n = n /2
// 因此在第一次会遍历`log2(n)`次
} while ( (n >>>= 1) != 0);
// 遍历过程当中没出现过时`Entry`的状况下会返回是否有清理的标记.
return removed;
}
复制代码
Entry
,并作是否须要resize
的判断private void rehash() {
// 清理过时Entry
expungeStaleEntries();
// 初始阈值threshold为10
if (size >= threshold - threshold / 4)
resize();
}
复制代码
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();
// k为空即表示为过时节点,立即清理了.
if (k == null) {
e.value = null;
} 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;
}
复制代码
ThreadLocal
的内部方法由于逻辑都不复杂,不须要单独出来看,就直接全放一块了.ThreadLocal
的精华只要仍是在TheradLocalMap
和这种空间换时间的结构.
getMap
方法获取当前线程绑定的threadLocals
ThreadLocal
对象为参数获取对应的Entry
对象.为空跳到第四步Entry
对象中的value
,并返回setInitialValue
方法,// 直接获取线程私有的数据
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// getMap其实很简单就是获取`t`中的`threadLocals`,代码在`工具方法`中
ThreadLocalMap map = getMap(t);
if (map != null) { // 3.
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) { // 2.
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 1.
}
// 这个方法只有在上面`1.`处调用...不知道为何map,thread不直接传参
// 该方法的功能就是为`Thread`设置`threadLocals`的初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map不为null代表是从上面的`2.`处进入该方法
// 已经初始化`threadLocals`,但并未找到当前对应的`Entry`
// 因此此时直接添加`Entry`就行
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// 初始值,`protected`方便子类继承,并定义本身的初始值.
protected T initialValue() {
return null;
}
// 建立并赋值`threadLocals`的方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
ThreadLocalMap
对象.map
不为空时,直接set就好map
为空时须要先建立并赋值.public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // .1
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
复制代码
t
中保留的ThreadLocalMap
类型的对象ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
复制代码
首先对于对做为key
的ThreadLocal
对象,由于是弱引用咱们彻底不用担忧,强引用断开以后天然会被GC
回收.安全
再来看value
,按照上面所说的做为成员变量存储在每一个Thread
实例的threadLocals
才是存储数据的对象,那么它的生命周期是和Thread
相同的,即便将ThreadLocal
被GC
回收, 但对应的value
对象仍然存在thread -> threadLocals -> value引用 -> value对象
的引用关系,因此GC
会认为它可达,并不会作回收处理,但在咱们现有的代码中并无可以跳过key
去获取value
的,也就是说实际上value
已经不可达了.这样就形成了内存泄漏.数据结构
value
的引用关系,就是讲value
引用置null
.ThreadLcoalMap
的方法多处调用了expungeStaleEntry
,cleanSomeSlots
检查数组中的Entry
对象是否过时,也就是key
是否为空.并发问题
在我理解中就是多线程状况下对共享资源的合理使用,像是ReentrantLock
,Synchronized
都是帮咱们解决共享资源的使用问题.ThreadLocal
则帮咱们提供了另一种思路,就是在每个线程中保留副本,就是上文有提到的以空间换时间的形式保证资源的合理有序使用,因此我以为也是解决并发问题的一种思路.