We are all in the gutter, but some of us are looking at the stars
ThreadLocal 在大多数面试中经常会被问到,让你聊聊对它的认识理解以及原理,今天我们就谈谈ThreadLocal,希望大家看完之后能够在面试中游刃有余,信手捏来,好了,进入正题
ThreadLocal 是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示
public class ThreadLocalMain {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void print(String str){
System.out.println(str + ":"+threadLocal.get());
threadLocal.remove();
}
public static void main(String[] args){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量值
threadLocal.set("thread1");
print("thread1");
System.out.println("after remove+"+threadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程2中本地变量值
threadLocal.set("thread2");
print("thread2");
System.out.println("after remove+"+threadLocal.get());
}
});
thread1.start();
thread2.start();
}复制代码
输出结果: thread1:thread1 thread2:thread2 after remove+null after remove+null复制代码
先看threadlocal的几个方法:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
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();
}
//getMap方法
ThreadLocalMap getMap(Thread t) {
//获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
return t.threadLocals;
}
//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}复制代码
从上面代码可以看出 每个线程持有一个ThreadLocalMap对象 。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals
//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}复制代码
createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadlocals变量
ThreadLocalMap map = getMap(t);
//如果threadlocals不为空,则再map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}复制代码
public void remove() {
//获取当前线程绑定的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}复制代码
在使用ThreadLocal的时候,需要手动去remove掉Threadlocal中的ThreadlocalMap,避免内存泄漏
当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap里面就回存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。 如果当前线程一直存在而没有调用Threadlocal的remove方法,并且这时候其他地方还有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的 。但是考虑如果这个ThreadLocal变量没有了其他强依赖,而当前线程还存在的情况下,由于线程的ThreadLocalMap里面的key是弱引用, 则当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会被在gc的时候回收,但是对应value还是会造成内存泄露 ,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项,so,一定要记得remove,避免内存泄漏
从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。
下面我们分两种情况讨论:
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。