ThreadLocal源码解析
文章目录
- 一、概述
- 二、get()方法
- 三、set()方法
- 四、可能导致的内存泄漏问题
- 五、remove
- 六、思考:为什么要将ThreadLocalMap的value设置为强引用?
一、概述
ThreadLocal
是线程私有的,独立初始化的变量副本。存放在和线程进行绑定的ThreadLocalMap
中。ThreadLocalMap
的内部类Entry
,是一个键值对的结构,key是ThreadLocal
对象,value是某个变量在某个时刻的副本。并且Entry
继承了WeakReference
类,使其key(ThreadLocal
对象)成为弱引用
,如果未正确使用remove方法,可能会导致内存泄漏问题。
构造entry对象时,key使用父类的方法,被包装成弱引用。
ThreadLocalMap
是ThreadLocal
的一个静态内部类:
但是其初始化的操作,是绑定在每个线程中的,作为线程对象的属性
,随着线程对象的加载而初始化。
并且ThreadLocalMap
的内部,是一个entry键值对
数组的形式。也有初始容量和扩容机制。这一点和HashMap类似,区别在于,处理Hash冲突时,HashMap使用的是拉链法
,也就是对于Hash值相同的key,会形成一条链表乃至树化。而ThreadLocalMap使用的是开放定址法中的线性探测再散列
,即某一个key计算出的Hash值,该位置已经有了元素,则会沿着数组下标依次向后寻找空位。
线性探测再散列
ThreadLocal通常会作为类的属性
,并且用static
关键字修饰。原因在于ThreadLocal需要从属于某个类,而不是具体的实例。
二、get()方法
在get方法中,主要做了几件事:
- 获取ThreadLocalMap。
- ThreadLocalMap不为空,则获取ThreadLocalMap的Entry 对象,并且对象不为空,就返回该Entry 对象的value。
- ThreadLocalMap为空,就执行初始化操作。
public T get() {
//获取当前线程对象(调用的是本地方法)
Thread t = Thread.currentThread();
//根据线程对象,获取到与该线程一一对应的ThreadLocalMap(ThreadLocalMap 是线程对象的属性)
ThreadLocalMap map = getMap(t);
//map第一次是为空的
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() {
//一、初始化value为null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//二、用当前线程对象,获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//map不为空,就将当前的ThreadLocal对象作为key,null作为value,构造Entry对象。
if (map != null)
map.set(this, value);
else
//三、否则初始化map
createMap(t, value);
//返回null
return value;
}
//一
protected T initialValue() {
return null;
}
//二
ThreadLocalMap getMap(Thread t) {
//ThreadLocal.ThreadLocalMap threadLocals = null;
return t.threadLocals;
}
//三
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
三、set()方法
set方法和get方法大同小异,也是根据当前线程获取ThreadLocalMap ,然后判空,执行set操作还是初始化操作。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//一、向ThreadLocalMap 的entry中插入元素的操作。
map.set(this, value);
else
createMap(t, value);
}
四、可能导致的内存泄漏问题
先看一下这段代码,声明了一个线程池,以及LocalVariable
静态内部类,其中的成员变量是5M大的数组。并且还有一个ThreadLocal
属性,将LocalVariable
作为key包装成了弱引用。
public class ThreadLocalMemoryLeak {
private static final int TASK_LOOP_SIZE = 500;
/*线程池*/
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024 * 1024 * 5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> threadLocalLV;
public static void main(String[] args) throws InterruptedException {
SleepTools.ms(4000);
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
SleepTools.ms(500);
//
// LocalVariable localVariable = new LocalVariable();
//
//
// ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
// oom.threadLocalLV = new ThreadLocal<>();
// oom.threadLocalLV.set(new LocalVariable());
//
// oom.threadLocalLV.remove();
System.out.println("use local varaible");
}
});
SleepTools.ms(100);
}
System.out.println("pool execute over");
}
}
如果没有加入ThreadLocal,而是仅仅在空跑,使用jvisualvm进行观察,看到的内存使用情况,是相对比较平稳的:
接着打开注释:
ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
oom.threadLocalLV = new ThreadLocal<>();
oom.threadLocalLV.set(new LocalVariable());
发现在运行过程中,内存使用情况起伏明显,多次触发gc。
并且最终程序执行完成后,内存还处于较高的水平,也就是说明堆中还存在很多没有被回收的垃圾对象。
为什么和没有使用ThreadLocal之前,会有如此大的差距?原因在于,每次垃圾回收时,作为弱引用的Entry的key:ThreadLocal对象会被回收,但是其value没有被回收。(在JVM停止时统一销毁)。
而每个线程中都存在一个5M的强引用对象没有被回收。
解决方式是,在ThreadLocal使用完成后,手动调用remove方法进行清除:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
五、remove
在remove方法中,主要完成了三件事:
- 获取哈希表。
- 计算索引,根据 key(即 ThreadLocal 对象)的哈希码,计算它在哈希表中的索引。
- 遍历表格中的链表查找匹配的条目。
而expungeStaleEntry方法中,除了将value和entry的引用全部置空以外,还会继续向后扫描,将ThreadLocalMap中弱引用的key已经被回收的entry的value置空。
(get和set方法中也有该实现)
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
//将key的引用置空
e.clear();
//一
expungeStaleEntry(i);
return;
}
}
}
//一
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//将指定下标的value的引用置空
tab[staleSlot].value = null;
//将指定下标的entry置空
tab[staleSlot] = null;
//table的长度减少
size--;
//这部分代码会继续扫描从 staleSlot 后的条目。如果遇到 ThreadLocal 对象已经被回收(k == null),则清除该条目。
//否则,重新计算哈希位置并尝试将条目移动到新的位置。
//这里使用了一个 while 循环来处理条目的重新定位,确保哈希表在移除过期条目后依然保持正确。
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;
}
六、思考:为什么要将ThreadLocalMap的value设置为强引用?
线程本地存储的一个核心需求是,数据必须与线程的生命周期
绑定,直到该线程结束或者显式移除该值。如果线程本地存储的 value 被弱引用,就无法保证它在使用时的可用性,可能会导致意外的回收和不可预期的行为。如果 ThreadLocalMap 的 value 被设置为弱引用,那么 ThreadLocalMap 中的条目就可能会在垃圾回收时被回收,因为 value 被弱引用(即没有强引用指向它)。这就可能导致在需要访问该值时,数据已经被清理掉。
最主要的点在于,ThreadLocalMap 的生命周期跟 Thread 一样长。