起源:blog.csdn.net/zzg1229059735/article/details/82715741
本次给大家介绍重要的工具 ThreadLocal。解说内容如下,同时介绍什么场景下产生内存透露,如何复现内存透露,如何正确应用它来防止内存透露。
- ThreadLocal 是什么?有哪些用处?
- ThreadLocal 如何应用
- ThreadLocal 原理
- ThreadLocal 应用有哪些坑及注意事项
1. ThreadLocal 是什么?有哪些用处?
首先介绍 Thread 类中属性 threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
咱们发现 Thread 并没有提供成员变量 threadLocals 的设置与拜访的办法,那么每个线程的实例 threadLocals 参数咱们如何操作呢?这时咱们的配角:ThreadLocal 就退场了。
所以有那么一句总结:ThreadLocal 是线程 Thread 中属性 threadLocals 的管理者。
也就是说咱们对于 ThreadLocal 的 get, set,remove 的操作后果都是针对以后线程 Thread 实例的 threadLocals 存,取,删除操作。相似于一个开发者的工作,产品经理左右不了,产品经理只能通过技术 leader 来给开发者分配任务。上面再举个栗子,进一步阐明他们之间的关系:
- 每个人都一张银行卡
- 每个人每张卡都有肯定的余额。
- 每个人获取银行卡余额都必须通过该银行的管理系统。
- 每个人都只能获取本人卡持有的余额信息,别人的不可拜访。
映射到咱们要说的 ThreadLocal
- card 相似于 Thread
- card 余额属性,卡号属性等相似于 Treadlocal 外部属性汇合 threadLocals
- cardManager 相似于 ThreadLocal 治理类
那 ThreadLocal 有哪些利用场景呢?
其实咱们无意间曾经时时刻刻在应用 ThreadLocal 提供的便当,如果说多数据源的切换你比拟生疏,那么 spring 提供的申明式事务就再相熟不过了,咱们在研发过程中无时无刻不在应用,而 spring 申明式事务的重要实现根底就是 ThreadLocal,只不过大家没有去深入研究 spring 申明式事务的实现机制。前面有机会我会给大家介绍 spring 申明式事务的原理及实现机制。
原来 ThreadLocal 这么弱小,但利用开发者应用较少,同时有些研发人员对于 ThreadLocal 内存透露,等潜在问题,不敢试用,恐怕这是对于 ThreadLocal 最大的误会,前面咱们将会仔细分析,只有依照正确应用形式,就没什么问题。如果 ThreadLocal 存在问题,岂不是 spring 申明式事务是咱们程序最大的潜在危险吗?
2.ThreadLocal 如何应用
为了更直观的领会 ThreadLocal 的应用咱们假如如下场景
- 咱们给每个线程生成一个 ID。
- 一旦设置,线程生命周期内不可变动。
- 容器流动期间不能够生成反复的 ID
咱们创立一个 ThreadLocal 治理类:
测试程序如下:咱们同一个线程一直 get,测试 id 是否变动,同时测试实现后咱们就将其开释掉。
在主程序中咱们开启多个线程测试不通线程之间是否会影响
不出意外咱们的后果为:
后果:的确是不同线程间 id 不同,雷同线程 id 雷同。
3.ThreadLocal 原理
①ThreadLocal 类构造及办法解析:
上图可知:ThreadLocal 三个办法 get, set , remove 以及外部类 ThreadLocalMap
②ThreadLocal 及 Thread 之间的关系:
从这张图咱们能够直观的看到 Thread 中属性 threadLocals,作为一个非凡的 Map,它的 key 值就是咱们 ThreadLocal 实例,而 value 值这是咱们设置的值。
③ThreadLocal 的操作过程:
咱们以 get 办法为例:
其中 getMap(t) 返回的就上以后线程的 threadlocals,如下图,而后依据以后 ThreadLocal 实例对象作为 key 获取 ThreadLocalMap 中的 value,如果首次进来这调用 setInitialValue()
set 的过程也相似:
留神:ThreadLocal 中能够间接 t.threadLocals 是因为 Thread 与 ThreadLocal 在同一个包下,同样 Thread 能够间接拜访 ThreadLocal.ThreadLocalMap threadLocals = null; 来进行申明属性。
4.ThreadLocal 应用有哪些坑及注意事项
我常常在网上看到骇人听闻的题目,ThreadLocal 导致内存透露,这通常让一些刚开始对 ThreadLocal 了解不透彻的开发者,不敢贸然应用。越不必,越生疏。这样就让咱们错失了更好的实现计划,所以敢于引入新技术,敢于踩坑,能力不断进步。
咱们来看下为什么说 ThreadLocal 会引起内存透露,什么场景下会导致内存透露?
先回顾下什么叫内存透露,对应的什么叫内存溢出
- ①Memory overflow: 内存溢出,没有足够的内存提供申请者应用。
- ②Memory leak: 内存透露,程序申请内存后,无奈开释已申请的内存空间,内存透露的沉积终将导致内存溢出。
显然是 TreadLocal 在不标准应用的状况下导致了内存没有开释。
红框里咱们看到了一个非凡的类 WeakReference,同样这个类,利用开发者也同样很少应用,这里简略介绍下吧
既然 WeakReference 在下一次 gc 行将被回收,那么咱们的程序为什么没有出问题呢?
①所以咱们测试下弱援用的回收机制:
这一种存在强援用不会被回收。
这里没有强援用将会被回收。
下面演示了弱援用的回收状况,上面咱们看下 ThreadLocal 的弱援用回收状况。
②ThreadLocal 的弱援用回收状况
如上图所示,咱们在作为 key 的 ThreadLocal 对象没有内部强援用,下一次 gc 必将产生 key 值为 null 的数据,若线程没有及时完结必然呈现,一条强援用链 Threadref
–>Thread
–>ThreadLocalMap
–>Entry
,所以这将导致内存透露。
上面咱们模仿复现 ThreadLocal 导致内存透露:
1. 为了成果更佳显著咱们将咱们的 treadlocals 的存储值 value 设置为 1 万字符串的列表:
class ThreadLocalMemory {
// Thread local variable containing each thread's ID
public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
@Override
protected List<Object> initialValue() {List<Object> list = new ArrayList<Object>();
for (int i = 0; i < 10000; i++) {list.add(String.valueOf(i));
}
return list;
}
};
// Returns the current thread's unique ID, assigning it if necessary
public List<Object> get() {return threadId.get();
}
// remove currentid
public void remove() {threadId.remove();
}
}
测试代码如下:
public static void main(String[] args)
throws InterruptedException {
// 为了复现 key 被回收的场景,咱们应用长期变量
ThreadLocalMemory memeory = new ThreadLocalMemory();
// 调用
incrementSameThreadId(memeory);
System.out.println("GC 前:key:" + memeory.threadId);
System.out.println("GC 前:value-size:" + refelectThreadLocals(Thread.currentThread()));
// 设置为 null,调用 gc 并不一定触发垃圾回收,然而能够通过 java 提供的一些工具进行手工触发 gc 回收。memeory.threadId = null;
System.gc();
System.out.println("GC 后:key:" + memeory.threadId);
System.out.println("GC 后:value-size:" + refelectThreadLocals(Thread.currentThread()));
// 模仿线程始终运行
while (true) {}}
此时咱们如何晓得内存中存在 memory leak 呢?
咱们能够借助 jdk 提供的一些命令 dump 以后堆内存,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>
而后咱们借助 MAT 可视化剖析工具,来查看对内存,剖析对象实例的存活状态:
首先关上咱们工具提醒咱们的内存透露剖析:
这里咱们能够确定的是 ThreadLocalMap 实例的 Entry.value 是没有被回收的。
最初咱们要确定 Entry.key 是否还在?关上 Dominator Tree,搜寻咱们的 ThreadLocalMemory,发现并没有存活的实例。
以上咱们复现了 ThreadLocal 不正当应用,引起的内存透露。demo 在这里。
所以咱们总结了应用 ThreadLocal 时会产生内存透露的前提条件:
- ①ThreadLocal 援用被设置为 null,且前面没有 set,get,remove 操作。
- ②线程始终运行,不进行。(线程池)
- ③触发了垃圾回收。(Minor GC 或 Full GC)
咱们看到 ThreadLocal 呈现内存透露条件还是很刻薄的,所以咱们只有毁坏其中一个条件就能够防止内存透露,单但为了更好的防止这种状况的产生咱们应用 ThreadLocal 时恪守以下两个小准则:
- ①ThreadLocal 申明为 private static final。
-
- Private 与 final 尽可能不让别人批改变更援用,
- Static 示意为类属性,只有在程序完结才会被回收。
- ②ThreadLocal 应用后务必调用 remove 办法。
-
- 最简略无效的办法是应用后将其移除。
以上。
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)
2. 劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4.Spring Boot 2.6 正式公布,一大波新个性。。
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!