前言
在面试环节中,考察 "ThreadLocal" 也是面试官的家常便饭,所以对它理解透彻,是非常有必要的.
有些面试官会开门见山的提问:
"知道 ThreadLocal 吗?"
"讲讲你对 ThreadLocal 的理解"
当然了,也有面试官会慢慢引导到这个话题上,比如提问 "在多线程环境下,如何防止自己的变量被其它线程篡改",将主动权交给你自己,剩下的靠自己发挥.
那么 ThreadLocal 可以做什么,在了解它的应用场景之前,我们先看看它的实现原理,只有知道了实现原理,才好判断它是否符合自己的业务场景.
ThreadLocal 是什么
首先,它是一个数据结构,有点像 HashMap,可以保存 "key : value" 键值对,但是一个 ThreadLocal 只能保存一个,并且各个线程的数据互不干扰.
ThreadLocal < String > localName = new ThreadLocal();
localName.set("占小狼");
String name = localName.get();
在线程 1 中初始化了一个 ThreadLocal 对象 localName,并通过 set 方法,保存了一个值占小狼,同时在线程 1 中通过 localName.get() 可以拿到之前设置的值,但是如果在线程 2 中,拿到的将是一个 null.
这是为什么,如何实现?不过之前也说了,ThreadLocal 保证了各个线程的数据互不干扰.
看看 set(T value) 和 get() 方法的源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以发现,每个线程中都有一个 ThreadLocalMap 数据结构,当执行 set 方法时,其值是保存在当前线程的 threadLocals 变量中,当执行 set 方法中,是从当前线程的 threadLocals 变量获取.
所以在线程 1 中 set 的值,对线程 2 来说是摸不到的,而且在线程 2 中重新 set 的话,也不会影响到线程 1 中的值,保证了线程之间不会相互干扰.
那每个线程中的 ThreadLoalMap 究竟是什么?
ThreadLoalMap
本文分析的是 1.7 的源码.
从名字上看,可以猜到它也是一个类似 HashMap 的数据结构,但是在 ThreadLocal 中,并没实现 Map 接口.
在 ThreadLoalMap 中,也是初始化一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不过这里的 key 永远都是 ThreadLocal 对象,是不是很神奇,通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中.
这里需要注意的是,ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap 很大的区别是,Entry 中没有 next 字段,所以就不存在链表的情况了.
hash 冲突
没有链表结构,那发生 hash 冲突了怎么办?
先看看 ThreadLoalMap 中插入一个 key-value 的实现
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;
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();
}
每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal 对象,hash 值就增加一个固定的大小
0x61c88647
.
在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i,过程如下: 1,如果当前位置是空的,那么正好,就初始化一个 Entry 对象放在位置 i 上; 2,不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,那么重新设置 Entry 中的 value; 3,很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;
这样的话,在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置
可以发现,set 和 get 如果冲突严重的话,效率很低,因为 ThreadLoalMap 是 Thread 的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为.
内存泄露
ThreadLocal 可能导致内存泄漏,为什么? 先看看 Entry 的实现:
static class Entry extends WeakReference < ThreadLocal < ?>>{
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal < ?>k, Object v) {
super(k);
value = v;
}
}
通过之前的分析已经知道,当使用 ThreadLocal 保存一个 value 时,会在 ThreadLocalMap 中的数组插入一个 Entry 对象,按理说 key-value 都应该以强引用保存在 Entry 对象中,但在 ThreadLocalMap 的实现中,key 被保存到了 WeakReference 对象中.
这就导致了一个问题,ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露.
如何避免内存泄露
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get(),set() 可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象.
如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法.
ThreadLocal < String > localName = new ThreadLocal();
try {
localName.set("占小狼");
// 其它业务逻辑
} finally {
localName.remove();
}
End 我是占小狼 如果读完觉得有收获的话,欢迎点赞加关注
来源: https://juejin.im/post/5a64a581f265da3e3b7aa02d