线程变量存储在ThreadLocalMap中,ThreadLocal只是作为key存在,而ThreadLocalMap中key为ThreadLocal的若引用。
弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value,即发生内存泄漏。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看到,get()方法底层就是调用Threadlocal的getEetry()方法
private Entry getEntry(ThreadLocal<?> key) {
//散列获取index
int i = key.threadLocalHashCode & (table.length - 1);
//从数组中取值
Entry e = table[i];
//判断是否为空,Entry的key是否与传入的key一致
if (e != null && e.get() == key)
return e;
//处理脏Entry或散列冲突
else
return getEntryAfterMiss(key, i, e);
}
可以看到,如果Entry不为null,且传入的key与Entry中存储的key一致,则直接返回。否则要调用getEntryAfterMiss()进行处理。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
//如果Entry的key和传入的key一致,则返回e(说明不是脏entry)
if (k == key)
return e;
//如果为空,说明是脏key,进行处理
if (k == null)
expungeStaleEntry(i);
//非空,key不一致,说明是散列冲突,则继续向后循环查找
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
getEntryAfterMiss()的处理可以分为三种情况:
1和3情况都很简单,所以我们看第2中情况,jdk的编写者做了怎样处理,也就是expungeStaleEntry()方法。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清除saleslot位置的脏Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
//长度减一
size--;
//重新散列,直到遇到table[i]==null时
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//查找过程中再次遇到脏Entry,清除掉
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//重新散列该Entry
int h = k.threadLocalHashCode & (len - 1);
//如果h != i,说明之前该Entry存在散列冲突,现在它前面的脏Entry被清理
//该Entry需要向前移动,防止下次get()或set()的时候再次因散列冲突而查找
//到null值
if (h != i) {
//当前位置置空
tab[i] = null;
//从初次散列的位置开始找,直到找到一个空位置
while (tab[h] != null)
h = nextIndex(h, len);
//赋值
tab[h] = e;
}
}
}
//这个i,即tab[i]==null的位置
return i;
}
首先清除当前位置的脏Entry,然后向后遍历直到table[i]==null。在遍历的过程中如果再次遇到脏Entry就会清理,如果没有遇到就会重新散列当前遇到的Entry。如果重新散列得到的下标h与当前下标i不一致,说明该Entry被放入Entry数组的时候发生了散列冲突(其位置通过再散列被向后偏移了),现在其前面的脏Entry已经被清除,所以当前Entry应该向前移动,补上空位置。否则下次调用set()或get()方法查找该Entry的时候会查找到位于其之前的null值。
该方法会清理当前脏Entry位置,直到tab[i]==null之间的所有脏Entry。保证紧接的下次获取该Entry的操作不会读取脏Entry和null值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
与get()方法一样,也是通过调用底层的ThreadLocalMap的set()方法来设置线程变量。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//散列
int i = key.threadLocalHashCode & (len-1);
//tab[i]!=null,即散列位置存在一个Entry(可能是原来的Entry,可能是散列冲突,也可能是脏Entry)
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//拿到散列位置对应的key
ThreadLocal<?> k = e.get();
//1. 如果key一致,则赋值(说明是赋值为原来的Entry,ThreadLocal强引用还存在)
if (k == key) {
e.value = value;
return;
}
//2. 如果key为null,则先处理脏数据再赋值(散列位置是脏数据,ThreadLocal强引用不存在,key被GC)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
//3. 上述两种都不是即为散列冲突,向后查找即可
}
//4. tab[i]==null,即散列位置不存在Entry,new一个新的放入即可
tab[i] = new Entry(key, value);
int sz = ++size;
//插入后需要检测和清理脏Entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以分为4种情况:
1和3情况不需要多讲,主要看看对于2和4情况,大佬是如何处理的:
replaceStaleEntry()方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
// 向前环形查找,直到tab[i]==null,即找到散列中脏Entry的起始位置,为下一步向后清理脏Entry做准备
//slotEoExpung负责在查找中保存脏Entry的位置
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//判断是否为脏Entry
if (e.get() == null)
//记录位置
slotToExpunge = i;
// 向后查找,直到tab[i]==null或找到对应的key
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与staleSlot位置(即第一次散列位置)的Entry做交换
//防止下次set()或get()时出现散列冲突读到null值,维护hash表顺序
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//true: 向前查找没找到脏Entry,向后查找到可覆盖key
//false:向前查找找到脏Entry,向后查找到可覆盖key
if (slotToExpunge == staleSlot)
// 如果slotToExpunge == staleSlot,则说明slotToExpunge到i之间没有脏Entry
//所以更新脏Entry位置,为后面的清理做准备
slotToExpunge = i;
//先调用expungeStaleEntry()清理从上面记录slotToExpunge(最前面的脏Entry位置)
//到tab[i]==null位置的脏数据
//再调用cleanSomeSlots()向后扫描清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// key为null,说明是脏Entry,如果向前查找没找到脏Entry,则更新脏Entry位置为当前位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//向后查找没找到可覆盖的key,说明为新Entry,新建一个放入staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//true: 如果slotToExpunge在初始位置,说明只有初始位置一个脏Entry(已经被新Entry覆盖)
//false:如果slotToExpunge不在初始位置,说明其后面存在脏Entry,继续清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
首先明确两点:
此方法也可以分为4中情况:
向前查找到脏Entry,向后查找到可覆盖的Entry
向前搜索脏Entry 向后搜索
<-------------------- ----------------->
(脏Entry)
脏Entry 起始位置 可覆盖Entry
| | |
------|--------------------|-------------------------|----------- Entry数组
slotToExpunge staleSlot <--------> key一致 i
| 交换
|
|------------------------------------------>
cleanSomeSlot()清理脏Entry
先向前搜索脏Entry,找到后通过slotToExpunge下标,直到tab[i]==null。这一步是为了获取到最前面的脏Entry位置,方便后面从头进行清理。
然后向后查找可以覆盖的key,直接到tab[i]==null。找到后直接赋值,然后交换当前Entry和初始staleSlot位置的Entry。因为初始位置的脏Entry在后面会被清理,先将当前Entry换过去,防止下次获取Entry时读到null值。
因为向前遍历时搜索到了脏Entry,slotToExpunge已经被改变与staleSlot并不相等。最后直接从slotToExpunge位置开始清理即可。
expungeStaleEntry()只能清理一次散列冲突范围内的脏Entry,所以最后会调用cleanSomeSlots()开始一次大范围扫描清理。
向前查找到脏Entry,向后没找到可覆盖的Entry
向前搜索脏Entry 向后搜索(未找到)
<-------------------- ----------------->
(脏Entry)
脏Entry 起始位置 tab[i]==null
| | |
------|--------------------|----------------------|-------------- Entry数组
slotToExpunge staleSlot
| |
| 插入新Entry
|------------------------------------------>
cleanSomeSlot()清理脏Entry
向前查找脏Entry,然后记录。向后查找可覆盖的Entry,但是并没有找到。在向后遍历的过程中,每当找到脏Entry(k == null),就会比较 slotToExpunge == staleSlot,因为前面找到了脏Entry,所以slotToExpunge位置已经改变,不需要变动。
因为向后没有找到可覆盖的Entry,所以为新Entry,直接new一个放入初始的位置(staleSlot)。然后从slotToExpunge位置开始清理。
向前搜索没有找到脏Entry,向后查找到可覆盖的Entry
向前搜索脏Entry(没找到) 向后搜索
<-------------------- ----------------->
(脏Entry)
tab[i]==null 起始位置 脏Entry 可覆盖Entry
| | | |
------|--------------------|------------|-------------|----------- Entry数组
staleSlot slotToExpunge key一致 i
| <---------------------> |
交换
|
|-------------------------------->
cleanSomeSlot()清理脏Entry
向前搜索脏Entry,但并未找到,所以此时slotToExpunge==staleSlot。向后查找可覆盖的Entry,最终找到。此处有两种情况:
3.1 向后查找的过程中,发现了脏Entry(k == null):此时会将当前位置i赋值为slotToExpunge,因为向前搜索并未发现脏Entry,当前位置即为最前面的脏Entry位置。
继续寻找发现可覆盖的Entry,则进行赋值,交换当前Entry的位置到staleSlot位置。然后进行判断:
因为在向后查找的过程中发现了脏Entry,slotToExpunge已经更改,所以为false。最后从slotToExpunge开始清理。
3.2 向后查找的过程中没有发现脏Entry就找到可覆盖的Entry了:则直接赋值,交换当前Entry的位置到staleSlot位置。然后进行判断:
因为在向后查找的过程中没有发现脏Entry,slotToExpunge==staleSlot,所以为true。此时当前位置为最前面的脏Entry位置,所以把i赋值给slotToExpunge。最后从slotToExpunge开始清理。
向前搜索没有找到脏Entry,向后查找没找到可覆盖的Entry
向前搜索脏Entry(未找到) 向后搜索(未找到)
<-------------------- -------------------------->
(脏Entry)
tab[i]==null 起始位置 脏Entry tab[i]==null
| | | |
------|--------------------|------------|-------------|----------- Entry数组
staleSlot slotToExpunge
| |
新插入Entry |
|-------------------------------->
cleanSomeSlot()清理脏Entry
向前查找未找到脏Entry,此时slotToExpunge==staleSlot。向后查找可覆盖Entry过程中发现脏Entry,此时判断:
当前位置即为最前面脏Entry位置,所以i赋值给slotToExpunge。找到最后没找到,则新建一个Entry放入初始的staleSlot位置。
此时判断:
如果前面未发现脏Entry,则slotToExpunge还在初始位置,说明只有初始位置一个脏Entry(已经被新Entry覆盖),直接返回即可。
如果前面发现了脏Entry,则slotToExpunge位置已经改变,从slotToExpunge位置开始清理。
综上,该方法就是在定位最前面的脏Entry位置,然后通过cleanSomeSlots()方法向后清理。
cleanSomeSlots()
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];
//判断脏Entry
if (e != null && e.get() == null) {
//遇到脏Entry重新赋值n
n = len;
removed = true;
//清理i到tab[i]==null
i = expungeStaleEntry(i);
}
//n控制循环次数,如果一个脏Entry都没遇到,则循环log2(n)+1 次
//遇到脏Entry,则会不断扩大向后扫描的范围,直到扫描不到脏Entry
} while ( (n >>>= 1) != 0);
return removed;
}
首先我们要清楚expungeStaleEntry(int staleSlot)方法清除的范围是多大:
假设a,b,c为三个散列后的hashcode,均为0,此时a因为失去强引用被GC,成为脏Entry。因为ThreadLocalMap采用线性探测再散列解决散列冲突,所以最后结果如下:
hashcode (a) b c
---0-----1-----2-----3-----4-----5----- Entry[]
key null 空
此时调用expungeStaleEntry(0)返回3,Entry[]变为:
hashcode b c
---0-----1-----2-----3-----4-----5----- Entry[]
key 空 空
清理的范围实际为[0,3),仅能保证下一次对hashcode为0的获取读不到脏Entry。
cleanSomeSlots()的第二个参数n就是弥补的措施,n用来控制循环的次数log2(n)
,如果在清理的过程中遇到脏Entry,则n被重置为数组长度len。即每遇到一次脏Entry,循环多执行log2(n)次。
通过这样不断的推进,达到清理大部分脏Entry的目的。
示例:默认Entry[]长度为16,因为是do-while循环,默认会执行4+1=5次。
hashcode (a) b c d e
---0-----1-----2-----3-----4-----5-----6-----7-----8-----9-----10-----11-----12- Entry[]
key null 空 空 空 null 空
|---------------> |---> | --> | --> | --------------> | -------> ........
log2(16)==4 3 2 1 读到脏key重置循环:4
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
调用底层的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)]) {
//key一致,命中,直接清理,然后调用expungeStaleEntry()保证下次对hashcode为i
//的Entry获取操作不再读到脏Entry
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
//没读到即为散列冲突,向后读取直到e==null结束
}
}
总结一下:整体来看,jdk的编写者认为,存在脏Entry的附近可能存在其他脏Entry,所以采用这种扫描的方式进行清理。
每次get()、set()、remove()操作后,都保证了下次对同一hashcode的读取不会再读到脏Entry。
至于为何不采用遍历数组的方式进行清理,可能是因为执行效率的考量吧。