ThreadLocal 的作用是针对目标字段,给每一个线程维护一个独立的变量副本,实现线程隔离,一个Thread中字段的修改不会影响到其他线程。
ThreadLocal是怎样实现线程独立的?
线程独立副本的实现核心依赖于ThreadLocalMap
这一数据结构,每一个 Thread 都维护一个 ThreadLocalMap,用于存储该线程的所有 ThreadLocal 变量及其对应的值。下面是 T ThreadLocal<T>.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();
}
- 获取当前所在的线程对象,获取到该线程维护的 ThreadLocalMap 对象。
- ThreadLocalMap 是一个类哈希表数据结构,key是 ThreadLocal 对象,value 是被包装的字段本身;
- 所以每次调用ThreadLocal.set()时,都是在所在Thread中的ThreadLocalMap中获取到备份字段,修改该备份字段,自然影响不到其他线程。
ThreadLocalMap的数据结构 – 散列表(线性开放寻址)
有一个误区是以为 ThreadLocalMap 底层是一个HashMap。
下面是 ThreadLocalMap 的部分源码:
static class ThreadLocalMap {
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
}
可以看出,Entry 是一个 key-value 结构
- key:弱引用的ThreadLocal对象,只要该对象没有其他强引用,就会被GC垃圾回收;
- value:被ThreadLocal包装的T对象
如下图:
用 Entry[] 数组来存储 Entry。接着再来看 ThreadLocalMap.set(ThreadLocal<?> key, Object 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();
}
可以看出,这里的哈希表实现 不同于HashMap的处理(发生哈希碰撞存成链表或红黑树),这里用到的是开放寻址法(线性探测)处理hash冲突,也就是当发生hash碰撞时,+1向后寻址,直到找到空位置。
另外,如果 size 达到 threshold,会调用 rehash() 方法进行扩容,重新计算所有键的存储位置,减少冲突。
ThreadLocal的内存泄漏问题
我们知道 ThreadLocalMap 的 key 是弱引用的 ThreadLocal,当该 ThreadLocal 没有强引用时就会被GC垃圾回收掉。然而,value 是强引用的,一旦key被垃圾回收,就会产生一个 key 为 null 的 Entry ,如果不采取措施,这个 Entry 将永远不会被回收,这时就可能发生内存泄漏问题。
因此在使用完ThreadLocal后,一定要记得 new ThreadLocal<>().remove();
操作!!!
探索散列算法 – 斐波那契(Fibonacci)散列法
// todo:待补充