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的实现原理:
先看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方法源码
在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(); }复制代码
remove方法的实现
public void remove() { //获取当前线程绑定的threadLocals ThreadLocalMap m = getMap(Thread.currentThread()); //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量 if (m != null) m.remove(this); }复制代码
在使用ThreadLocal的时候,需要手动去remove掉Threadlocal中的ThreadlocalMap,避免内存泄漏
为什么每次用完后remove
当一个线程调用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导致。
下面我们分两种情况讨论:
- 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就跟加锁完要解锁一样,用完就清理。
原文
https://juejin.im/post/5e7a0d4d6fb9a07cbd01fbdb
本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 我们来聊聊ThreadLocal