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就跟加锁完要解锁一样,用完就清理。