彻底理解ThreadLocal
-
场景
在做日志分析时,我们想对一次request请求进行链路追踪,如何实现? -
方案
首先定义一个标识本次request请求的变量,用来输出打印并进行跟踪时查找,但在并发场景下,这个变量可能会被其他request请求覆盖掉,所以我们要为每个request请求设置变量副本,供当前线程使用。并且一个request 请求即为一个线程。 -
实现
当在多线程并发情况下,有一个共享变量,不同线程设置不同值后,各线程只想获取自己设置的值,这个时候需要用到ThreadLocal。 -
源码
- 存储结构
// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储数据的哈希表
//这个table是一个Entry类型的数组,Entry是ThreadLocalMap的一个内部类。
//Entry 用于保存一个键值对,其中key以“弱引用”的方式保存。
private Entry[] table;
// table 中已存储的条目数
private int size = 0;
// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;
// 设置 threshold 的值
private void setThreshold(int len){
threshold = len *2/3;
}
- 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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先获取当前线程t,拿到线程的threadLocals属性(ThreadLocalMap类型),如果没有当前线程的map信息就创建createMap,否则进行set操作。
- 详细流程
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算要存储的索引位置
int i = key.threadLocalHashCode & (len-1);
// 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
// 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
if (k == key) {
e.value = value;
return;
}
// 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
tab[i] = new Entry(key, value);
// 增加 table 存储的条目数
int sz = ++size;
// 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}
- 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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
前面流程和set一致,当ThreadLocalMap 为空时调用setInitialValue方法,默认返回null,不为空时获取map的Entry对象返回结果value。
- 详细流程
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//在key计算hash的位置上直接查询,命中返回该entry
if (e != null && e.get() == key)
return e;
else
// 没有直接命中,调用getEntryAfterMiss
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 从i位置开始遍历,寻找key能对应上的entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// 遇到key为null的entry,调用expungeStaleEntry方法
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // 如果entry的key为null,则清除掉该entry
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
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) // 对该entry,重新进行hash并解决冲突
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i; // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
}
expungeStaleEntry方法不止清理了staleSlot位置上的entry,还把staleSlot之后的key为null的entry都清理了,并且顺带将一些有哈希冲突的entry给填充回可用的index中。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从上图中我们可以看到 entry 为弱引用,所以我们在执行完逻辑后一定要调用remove方法
- 原因
- 数据问题:线程是被线程池复用的,如果在线程处理业务结束的时候,不做remove操作,那么下个业务请求复用到这个线程的时候,也会用线程的ThreadLocal里面的变量执行业务逻辑。
- 内存泄漏:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。
- 如何解决或者防止内存泄漏
每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
- 如何获取主线程变量?
使用 InheritableThreadLocal
- 扩展之Synchonized的区别
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized本质的区别是:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。