作者简介:小明Java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公司后端高级工程师。
2022博客之星TOP3 | CSDN博客专家 | 后端领域优质创作者 | CSDN内容合伙人
InfoQ(极客邦)签约作者、阿里云专家 | 签约博主、51CTO专家 | TOP红人、华为云享专家
如果此文还不错的话,还请关注、点赞、收藏三连支持一下博主~
文末获取联系 精彩专栏推荐订阅收藏
专栏系列(点击解锁)
学习路线(点击解锁)
知识定位
Redis从入门到精通与实战
Redis从入门到精通与实战
围绕原理源码讲解Redis面试知识点与实战
MySQL从入门到精通
MySQL从入门到精通
全面讲解MySQL知识与企业级MySQL实战 计算机底层原理
深入理解计算机系统CSAPP
以深入理解计算机系统为基石,构件计算机体系和计算机思维
Linux内核源码解析
围绕Linux内核讲解计算机底层原理与并发
数据结构与企业题库精讲
数据结构与企业题库精讲
结合工作经验深入浅出,适合各层次,笔试面试算法题精讲
互联网架构分析与实战
企业系统架构分析实践与落地
行业最前沿视角,专注于技术架构升级路线、架构实践
互联网企业防资损实践
互联网金融公司的防资损方法论、代码与实践
Java全栈白宝书
精通Java8与函数式编程
本专栏以实战为基础,逐步深入Java8以及未来的编程模式
深入理解JVM
详细介绍内存区域、字节码、方法底层,类加载和GC等知识
深入理解高并发编程
深入Liunx内核、汇编、C++全方位理解并发编程
Spring源码分析
Spring核心七IOC/AOP等源码分析
MyBatis源码分析
MyBatis核心源码分析
Java核心技术
只讲Java核心技术
本文导读
一、ThreadLocal是什么
二、ThreadLocal的数据结构
三、ThreadLocal源码解析
1、 ThreadLocal的set()方法
2、 ThreadLocal的get()方法
3、 ThreadLocal的remove()方法
四、ThreadLocal使用场景
五、ThreadLocal内存泄露原因
六、如何正确的使用ThreadLocal
七、ThreadLocal为什么不将key设置为强引用
总结
本文讲解ThreadLocal是什么、ThreadLocal的数据结构以及ThreadLocal源码set()/get()/remove()解析,ThreadLocal使用场景,如何正确的使用ThreadLocal,ThreadLocal内存泄露原因。
ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
ThreadLocal 提供了线程本地的实例。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
ThreadLocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
1、每个Thread线程内部都有一个Map(ThreadLocalMap)
2、Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
3、Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
4、对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离互不干扰。
ThreadLocal set赋值的时候首先会获取当前线程thread,获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
public void set(T value) {
Thread t = Thread.currentThread(); // 1、获取当前线程
// 2、获取线程中的threadLocalMap ,如果threadLocalMap不为空直接更新要保存的变量值
// 否则创建threadLocalMap 并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value); // 初始化thradLocalMap 并赋值
}
ThreadLocalMap 是 ThreadLocal 的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
static class ThreadLocalMap {
static class Entry extends WeakReference> { // Java弱引用
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
public T get() {
Thread t = Thread.currentThread(); // 1、获取当前线程
ThreadLocalMap map = getMap(t); // 2、获取当前线程的ThreadLocalMap
if (map != null) { // 3、如果map数据不为空,获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果是数据为null则初始化,TheralLocalMap中存放key为threadLocal值为null
return setInitialValue();
}
remove方法直接将ThrealLocal对应的值从当前相差Thread中的ThreadLocalMap中删除,
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal 适用于如下两种场景:1、每个线程需要有自己单独的实例;2、实例需要在多个方法中共享,但不希望被多线程共享。
1、存储用户Session(不同线程获取到的用户信息不一样)
2、数据库连接,处理数据库事务
3、数据跨层传递
4、Spring使用ThreadLocal解决线程安全问题
Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。
主要两个原因:1、没有手动删除这个Entry;2、CurrentThread 当前线程依然运行
解决方案:1、只要在使用完下ThreadLocal,调用remove方法删除对应的Entry,就能避免内存泄漏。
2、由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null,也即entry(null,value),那这个entry对应的value永远无法访问到。如果ThreadLocal场景采用线程池,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
综上, ThreadLocal 内存泄漏的根源是:如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。
1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
如果key设计成强引用且没有手动remove(),ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。
当前线程始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry,Entry就不会被回收( Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏,也就是说ThreadLocalMap中的key使用了强引用是无法完全避免内存泄漏的
弱引用比强引用可以多一层保障弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。
本文讲解ThreadLocal是什么、ThreadLocal的数据结构以及ThreadLocal源码set()/get()/remove()解析,ThreadLocal使用场景,如何正确的使用ThreadLocal,ThreadLocal内存泄露原因。