JavaThreadLocal
Posted 绝世好阿狸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaThreadLocal相关的知识,希望对你有一定的参考价值。
ThreadLocal
作用
线程级别变量隔离,减少多线程访问变量时加锁带来的性能损耗。
缺点
使用不当可能导致oom。
原理
弱引用是精髓。
get方法
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();
}
ThreadLocal解决并发的思路就是在每一个线程里都存放一份变量的值,互不干扰。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,Thread对象里有一个ThreadLocalMap类型的变量,这本质是一个map,key是ThreadLocal实例,value是对应的值。可以看到get方法就是从当前线程对象中的map里取到对应的k-v,返回。
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
同理,set方法也是类似,将k-v塞到当前线程对象的map结构里。
关键看下这个map结构
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
map结构的Entry定义如上,可以看到整个Entry就是一个WeakReference类型。
private Entry[] table;
ThreadLocalMap的核心成员变量是Entry类型的数组。那么,这里的map结构其实是用指针退避法来解决hash冲突。正常的map结构是拉链法,冲突的key构成一个链表。而采用指针退避法的map,遇到hash冲突时,会找下一个slot。
我们可以从该map的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;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
首先计算出key的哈希值,然后计算出slot的下标i,从i开始找第一个为空的slot,也就是for循环结束后的语句。然后new出一个Entry实例,赋值给tab[i]。这是最常规的情况。别忘了,Entry类型是弱引用,所以,如果没有任何强引用指向,该对象可能被回收。这样就会命中for循环里的第二个if,需要将当前entry替换为新的值,也就是replaceStableEntry方法。这里不做深入。
同时,在方法的结尾处,也会调用cleanSomeSlots方法检测map里的无效弱引用数据,进行清理和rehash:
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;
}
遍历一遍slot,对无效弱引用调用expungeStaleEntry方法清理和rehash:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
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;
}
实际上,每一次的get set remove方法也会触发ThreadLocalMap对无效弱引用的清理和rehash操作。
弱引用的意义
如果是用强引用,只要线程存在,ThreadLocal对象就会被引用到,这样比较消耗内存。举个例子,web容器比如tomcat,我们可以用ThreadLocal变量存放请求上下文信息。一次请求中可能存了一个很冷门的数据,只在1%的请求中会被使用。当这一次请求用完后,接下来的99个请求都不会用,但是由于线程还是那一个,所以这个对象无法被回收。如果换成弱引用,这一次请求结束后,就没有强引用指向这个对象了,gc时就会将这个ThreadLocal变量指向的对象回收(回收之后,map里的entry就指向了null,会在get set remove方法执行时清理掉,同时rehash),节约了内存。
参考:https://juejin.cn/post/6844904046373896205
以上是关于JavaThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章