Java内存结构图
说明:
- 每个线程都会有一个局部变量 threadLocals,存放在各自线程栈帧局部变量表中,指向堆中的ThreadLocalMap实例对象
- 不同的线程在堆中对应不同的ThreadLocalMap实例对象
- ThreadLocalMap的key是ThreadLocal实例对象
每个线程拥有各自的ThreadLocalMap实例对象
在threadLocal set值的时候,若threadLocalMap为null,new一个ThreadLocalMap对象
所以每个线程都是新new的ThreadLocalMap对象,堆中是不同的实例
public class ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
ThreadLocalMap
- ThreadLocalMap数据结构是一个Entry数组,Entry的key是ThreadLocal类型,value是Objcet;
- Entry的一个key只对应一个value值,即一个线程中一个ThreadLocal实例中只能存一个数据(value),不同与HashMap等有拉链之类
- Entry数组初始容量为16
- ThreadLocalMap的负载因子为2/3,超过阈值便进行扩容
public class ThreadLocal<T> {
static class ThreadLocalMap {
// 主要因为key为ThreadLocal,所以继承弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//数组初始容量
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
//阈值
private int threshold; // Default to 0
//负载因子
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//初始数组容量、初始数组下标及元素、初始扩容阈值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
用线性探测法解决hash碰撞问题。
具体细分有4种情况:
- 通过hash计算后的槽位对应的Entry数据为空,直接将数据放到该槽位。对应:
tab[i] = new Entry(key, value);
- 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致,直接更新该槽位的数据
if (k == key) {
e.value = value;
return;
}
- 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry。遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key值相等的数据,直接更新即可。
- 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,执行replaceStaleEntry方法里面逻辑
具体可参考:https://blog.csdn.net/l18848956739/article/details/106122096
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
源码:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 线性探测法
for (Entry e = tab[i];
e != null;
//数组下标加1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key相等,直接更新value
if (k == key) {
e.value = value;
return;
}
// k为null,替换stale entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//数组下标i没有元素,直接添加
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
cleanSomeSlots
参考下面图示(图片来源:https://blog.csdn.net/l18848956739/article/details/106122096):
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) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
expungeStaleEntry
从第一个过期元素往后遍历,遍历到的元素
如果key为null,将value、tab[i]也置为null,数组大小减1;
如果key不为null,将元素在Entry数组中进行重新定位,使桶位置离正确index更近
直到碰到空的slot,终止探测。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
图示:
replaceStaleEntry方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标slotToExpunge。for循环迭代,直到碰到Entry为null结束。
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap的扩容
如果执行完启发式清理工作后,未清理到任何数据,且容量大于threashold值时,进行扩容
private void set(ThreadLocal<?> key, Object value) {
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
//探测式清理
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
进行扩容时,扩容后的容量是原容量的2倍,然后进行数组拷贝。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//扩容的容量是旧容量的2倍
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();
if (k == null) {
e.value = null; // Help the GC
} 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;
}
ThreadLocalMap.Entry的弱引用问题
public class ThreadLocal<T> {
static class ThreadLocalMap {
// 主要因为key为ThreadLocal,所以继承弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
}
}
}
用实例来说明,如下面一个方法中调用get方法获取ThreadLocal中存放的数据,并进行业务处理,且之后不会再使用了。threadLocal置为null
-
此时,如果Entry没有继承WeakReference,则ThreadLocalMap始终有强引用执行这个ThreadLocal对象实例,则堆中无法进行GC回收,一直占用着内存,除非ThreadLocalMap也可以回收了,ThreadLocal对象实例才能回收。
-
而如果是继承WeakReference,threadLocal置为null时,ThreadLocalMap中key不是强引用,ThreadLocal对象实例就可以被GC回收了。map的后续操作中,也会逐渐把对应的"stale entry"清理出去,避免内存泄漏。
而最优实践是,使用完 ThreadLocal 变量时,尽量用threadLocal.remove()来清除,避免 threadLocal=null 的操作。前者 remove() 会同时清除掉线程 threadLocalMap 里的 entry,算是彻底清除;而后者虽然释放掉了 threadLocal,但线程 threadLocalMap 里还有其"stale entry",后续还需要处理。
这里的弱引用可以首先由 gc 来判断 ThreadLocal 实例(userInfoLocal)是否真的可以回收,由 gc 回收的结果,间接告诉我们,key 为 null 了,这时候 value 也可以被清理了,并且最终通过高频操作get/set/remove封装好的方法进行清理。如果用强引用那么我们一直不知道这个entry是否可以被回收,除非强制每个coder在逻辑执行完的最后进行一次全局清理。
为什么value不用弱引用呢?
value 不像 key那样,还有一个外部的强引用(userInfoLocal),如果将 value设置为弱引用,可能在业务执行过程中发生了gc,value 就被清理了,业务后边取值会出错的。
public class ThreadLocalTest {
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void test() {
String str = threadLocal.get();
//do something
threadLocal.remove();
//threadLocal = null;
}
}
使用场景
数据库连接、Session管理
参考:
https://blog.csdn.net/l18848956739/article/details/106122096
https://blog.csdn.net/qq_34548229/article/details/107220749