ThreadLocal源码学习笔记
前言
首先先对ThreadLocalz做一个简单介绍,接着学习一下set,get,remove方法,接着简单分析一下为什么可能会存在内内存泄漏。
ThreadLocal是什么
ThreadLocal是一个以ThreadLoacl自身为键,任意对象为值的存储数据结构,它是附带在线程上的。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。
ThreadLocal的数据结构
图片来源https://www.jianshu.com/p/98b68c97df9b
- 从图中我们可以看出,每个Thread都有自己的ThreadLocalMap,可以存储很多Entry对象。
- ThreadLocalMap中的键值对为ThreadLocal和需要存储的对象。
- 但是,Thread中的ThreadLocalMap由ThreadLocal维护。
- 注意看图,一个ThreadLocal对象只能存储一个Entry,一个Entry中存储一对键值对ThreadLocal和value,如果需要存储多个对象变量,那么需要创建多个ThreadLocal对象进行set操作。
实现原理
- ThreadLocal如何保存各自线程的对象变量呢?通过源码可以看到,在ThreadLocal中有一个静态内部类,也就是前面提到的ThreadLocalMap,而Thread类中有ThreadLocalMap属性,默认为null,在每个ThreadLocalMap中如上图所示,有多个Entry,在每个Entry中存储了想要存储的对象变量。由于ThreadLocalMap对象是Thread类的自带属性,是和每个线程绑定在一起的,这就是线程隔离的原因。
- void set(Object value)在存储变量的时候,使用set方法将需要存储的对象变量存入到ThreadLocalMap中。注意,一个ThreadLocal对象只能存入一个对象变量。
- public Object get()在取出值的时候,使用get方法取出存储的值。
- public void remove()将当前存储的对象变量删除,减少内存,由于ThreadLocalMap是Thread类的自带属性,所以不使用remove方法,在当前线程结束时,ThreadLocalMap对象也会被回收。
先看个例子:创建多个线程,通过多个线程并行地对ThreadLocal对象进行set、get操作,并将值进行打印。观察打印的值是否和存的值一致。可以看出,创建了一个ThreadLocal变量,可以并行的对多个线程进行set和get想要的值。
package thread;
/**
* Created by Administrator on 2019\4\30 0030.
*/
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread_ thread_ = new Thread_(threadLocal, i);
new Thread(thread_).start();
}
}
}
class Thread_ implements Runnable {
ThreadLocal threadLocal;
int value;
Thread_(ThreadLocal threadLocal,int value) {
this.threadLocal = threadLocal;
this.value = value;
}
@Override
public void run() {
threadLocal.set("线程ID:"+Thread.currentThread().getName()+"的值:"+value);
System.out.println(threadLocal.get());
threadLocal.remove();
}
}
//输出结果:
//线程ID:Thread-0的值:0
//线程ID:Thread-1的值:1
//线程ID:Thread-2的值:2
//线程ID:Thread-4的值:4
//线程ID:Thread-5的值:5
//线程ID:Thread-6的值:6
//线程ID:Thread-3的值:3
//线程ID:Thread-7的值:7
//线程ID:Thread-9的值:9
//线程ID:Thread-8的值:8
set方法
public void set(T value) {
// 得到当前线程对象
Thread t = Thread.currentThread();
// 通过当前线程得到ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果map为不为null
if (map != null)
// 直接将value存入map,这里this是ThreadLocal自身对象
// 在创建的map中也存在hash冲突的情况,这里采用了开放地址法,详细可以看源码的这个方法
map.set(this, value);
else
// 否则创建ThreadLocalMap,再将value存入map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// 返回threadLocals对象,此属性是也就是Thread类的自带属性,一个ThreadLocalMap
return t.threadLocals;
}
set方法总结一下:
- 获得当前线程
- 通过当前线程对象获得当前线程的ThreadLocalMap属性。
- 判断map是否为null,不为null则将value存入map,这里也涉及到hash冲突问题以及扩容问题,hash冲突问题采用开放地址法进行处理,详细实现请看源码的这个方法。若为null,创建map,将value存入map。
所以可以看出,其实ThreadLocal相当于一个工具类,实质是将数据存在了每个Thread的map属性中。
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();
}
get方法总结一下:
通过当前线程拿到当前线程的map,如果map不为null,取出ThreadLocalMap的Entry,再取出值,如果map为null,那么通过setInitialValue进行返回,通过这个方法可以重写进行初始化,如果没有重写,返回null。
从下面的代码我们可以看到,Entry是ThreadLocalMap的静态内部类,它继承于虚引用,但是这里只能对ThreadLocal进行回收,那么value作为强引用,在只有ThreadLocal作为键值对的键被回收后,value没有被回收便会造成内存泄漏。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
图片来源下面的解释也来自这个链接:http://www.importnew.com/22039.html
ThreadLocal为什么会内存泄漏
这是因为ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal在下次jvm垃圾回收时被回收,这个时候就造成了Entry中的key已经为null,但是value仍然存在,但又无法通过键来找到value,因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
但这样也并不能保证ThreadLocal不会发生内存泄漏,例如:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。
为什么使用弱引用?
从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。很多文章大多分析ThreadLocal使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?
官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
- 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。
总结
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
参考文献
[1]https://www.jianshu.com/p/a1cd61fa22da
[2]https://www.jianshu.com/p/56f64e3c1b6c
[3]https://www.jianshu.com/p/98b68c97df9b