深入并发二 ThreadLocal源码与内存泄漏相关分析
这篇文章的主要内容是介绍ThreadLocal
类使用方法,源码实现,以及实际应用。ThreadLocal
实际上是在多线程编程的过程中,每个线程用来保存局部变量的一个类,用这个类保存的变量在属于各个线程独有,不会互相影响,那么我们就可以实现不同线程保存同一个变量的不同值。
ThreadLocal的使用
ThreadLocal
的使用十分方便,下面给出一个使用的例子,实现同一个变量在不同线程中有着不同的值,同时,这两个值不互相影响。
public static void main(String[] args) {
ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {
return 0;
});
new Thread(() -> {
value.set(10);
//Thread-0 10
System.out.println(Thread.currentThread().getName() + " " + value.get());
}).start();
new Thread(() -> {
//Thread-1 0
System.out.println(Thread.currentThread().getName() + " " + value.get());
value.set(3);
//Thread-1 3
System.out.println(Thread.currentThread().getName() + " " + value.get());
}).start();
}
上面的代码中,我们定义了一个变量value
,这个变量在不同线程中有不同的值,所以我们使用ThreadLocal
,初始化这个值为0。上面的代码十分简单,就不做详细讲解了。
ThreadLocal源码分析
下面我们来分析一下ThreadLocal
的底层实现。
实际上,每个Thread对象都持有一个ThreadLocalMap
的对象,里面保存了所有ThreadLocal
变量,这个map的key是ThreadLocal
变量对象,而值就是这个线程中ThreadLocal
对应的值。
ThreadLocalMap
实际使ThreadLocal
的一个静态内部类。
下面我们先来分析方法set()
。
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程所持有的map对象
ThreadLocalMap map = getMap(t);
if (map != null)
//以当前ThreadLocal为key,将value值加入map中
map.set(this, value);
else
//如果map对象还没有,那么调用初始化方法,并且将值插入
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//获取线程对象持有的map对象
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//初始化threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
然后我们来分析一下get()
方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程所持有的map对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//取出map中key为当前ThreadLocal对象的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
//如果存在,直接返回value
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;
}
在这里我们可以看到如果我们调用过ThreadLocal
对象的set方法给对象赋值的话,这是调用get方法去取值,会调用方法setInitialValue。所以,一般我们在初始化ThreadLocal
对象的时候,会重写方法initialValue()
,这样就不会发生get方法返回值为null的情况。
同时在java8之后,我们也可以采用最开始的例子中的方法来初始化ThreadLocal
对象。
ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {
return 0;
});
ThreadLocalMap分析与ThreadLocal导致内存泄露的问题分析
这里我们不去分析ThreadLocalMap
中方法的具体实现,它的大部分功能和一个普通的map相似,我们主要是要分析一下ThreadLocal
导致内存泄漏的原因。
首先,给出关键部分的代码
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
注意,ThreadLocalMap
中的key实际上是ThreadLocal
对象的弱引用。
那么什么是弱引用呢,既然有弱引用必然就有强引用。
实际上强引用就是我们正常使用new关键字创建的引用,** 弱引用指的其实是WeakReference
关键字包裹的引用,在GC的过程中,如果一个对象只有弱引用指向它的时候,这个对象就已经可以被GC回收了,而一个强引用只有当所有引用都不存在的时候才可以被回收。**
那么,在map中为什么要使用弱引用这种方式呢?请大家想想一种情况,我们有一个对象,这个对象有一个引用A,并且这个对象作为map的key存在,那么当我们不再使用这个对象的时候,我们将引用置为null,这是,假设map中的引用是强引用,那么由于map中依然有这个对象的引用,那么这个对象不能够被GC回收,这显然不是我们想要看到的场景,所以,一般来说map中的key一般使用弱引用,这样,当对象只有这一个引用的时候就可以及时被GC回收。
- 因此,我们就有
HashMap
和WeakHashMap
两个类,大家可以后续了解一下,两者的主要区别就在于key是强引用还是弱引用。 *
下面转回正题,关于ThreadLocal
导致内存泄漏的问题。
在这里,key值实际上是ThreadLocal
变量的弱引用,所以当我们的key变为空的时候这个引用就不存在了,那么我们也就无从得到value的值,这是value的值就变成了无法访问的值。
ThreadLocalMap
中实际上已经考虑到了这个问题,当我们调用ThreadLocal
中set、get和remove方法的时候,实际上是会检查key为null的情况,将这些内容清掉。
当线程的生命周期结束的时候后,实际上所有的ThreadLocalMap
都会被回收,因此,这种情况下不会造成内存泄漏。
这里引用StackOverFlow中一位答主给出的情况,详情见 java - ThreadLocal & Memory Leak - Stack Overflow
这里给出翻译。
举一个例子:
有一个服务器有一个线程池,这些线程会一直存活知道服务器停止。
一个web应用在一个类中使用了一个static的ThreadLocal
来存放一些线程局部变量,这个变量是web应用中里一个类的对象(SomeClass)。这些操作实在一个线程中进行的。
根据定义,一个ThreadLocal
的引用会一直存活,知道拥有这个对象的线程死亡或者ThreadLocal
对象本身是不可达的。
如果web应用在关闭之前没有成功清除ThreadLocal
的引用,那么这时会发生十分糟糕的事情:
因为线程不会死亡,并且ThreadLocal
对象依然指向着的引用是static的,那么,虽然应用已经停止了,ThreadLocal
对象依然指向着SomeClass的对象(一个web应用中的类)
这种情况的结果就是,web应用中的classloader不会被GC,这就意味着web应用中所有的类(以及所有的静态类)都仍然被装载(这会影响到PermGen)
每一次reload应用都会增加PermGen的使用,这样就会导致permgen leak
相信上面的解释已经十分清晰了。下面给出tomcat中出现的例子,这个bug已经被官方修复了。
MemoryLeakProtection - Tomcat Wiki
至此,我们对ThreadLocal
的了解已经十分深入了,在我们使用ThreadLocal
类的时候,一定要十分注意,防止发生内存泄漏。