当前位置:网站首页>ThreadLocal内存泄漏是伪命题?
ThreadLocal内存泄漏是伪命题?
2022-07-30 08:51:00 【Evan_L】
前言
在研究Mybatis使用多数据源时,发现都是底层的数据源切换逻辑都是基于ThreadLocal。隐约想起,ThreadLocal存在内存泄漏问题。
ThreadLocal
类注释
上Class的注释:
/** * This class provides thread-local variables. These variables differ from * their normal counterparts in that each thread that accesses one (via its * {@code get} or {@code set} method) has its own, independently initialized * copy of the variable. {@code ThreadLocal} instances are typically private * static fields in classes that wish to associate state with a thread (e.g., * a user ID or Transaction ID). * 这个class提供线程本地变量。这些变量不同于其普通变量在于, * 通过get/set方法访问时,每个线程都有自己唯一的独立初始化后的变量副本。 * 典型使用:在类中作为私有静态的ThreadLocal实例, * 期望每个线程都有一个自己的状态(例如:一个用户id或者事务id) * <p>For example, the class below generates unique identifiers local to each * thread. * A thread's id is assigned the first time it invokes {@code ThreadId.get()} * and remains unchanged on subsequent calls. * 例如:下面的class,为每个线程生成了唯一的独一无二的本地变量。 * 线程Id在第一次调用ThreadId.get()方法复制,并且在后续的调用中保持不变。 * <pre> * import java.util.concurrent.atomic.AtomicInteger; * * public class ThreadId { * // Atomic integer containing the next thread ID to be assigned * private static final AtomicInteger nextId = new AtomicInteger(0); * * // Thread local variable containing each thread's ID * private static final ThreadLocal<Integer> threadId = * new ThreadLocal<Integer>() { * @Override protected Integer initialValue() { * return nextId.getAndIncrement(); * } * }; * * // Returns the current thread's unique ID, assigning it if necessary * public static int get() { * return threadId.get(); * } * } * </pre> * <p>Each thread holds an implicit reference to its copy of a thread-local * variable as long as the thread is alive and the {@code ThreadLocal} * instance is accessible; after a thread goes away, all of its copies of * thread-local instances are subject to garbage collection (unless other * references to these copies exist). * 每个线程都持有一个具体的引用,指向它的线程本地副本变量。只要线程存活,ThreadLocal实例都可以访问。 * 线程死亡后,线程所有的线程本地实例副本都将称为垃圾回收的目标。(除非还有其他引用指向这些线程副本) * * @author Josh Bloch and Doug Lea * @since 1.2 */
可以看到,作者并没有提及内存泄漏的可能。只有最后这么一句话:(unless other references to these copies exist)。这也很正常啊,你的目标副本在线程之外还有引用,自然不能被回收。
背景小知识
这个东西简单说一下,就是每个线程保留一份自己的值。Thread.threadLocals成员变量是ThreadLocal#ThreadLocalMap类型。而该Map的Entry.Key使用的是WeakRefrence
。WeakRefrence,简单说,就是每次发生GC都会清理掉其所指向的对象。
PS:这里涉及到的知识点:Java的四大引用类型,
从百度借图:
场景一:
前提:线程存活,但由于发生了GC,key被回收了,但Entry依然存在,对value的引用依然存在。value自然就不能被回收了,value在这里可是强引用。
从这个场景看,如果线程存活时间足够长,key为null的Entry持续增长,确实存在内存泄漏。
场景二:
前提:线程死亡,个人分析:
图中的强引用,将随着线程的死亡而失去强引用,从而被回收。value也会被回收,只要没有其指向它的引用。
但这个结论跟《码出高效》的结论不同,P266说:
如果不进行remove操作,那么这个线程执行完成后,通过ThreadLocal对象持有的String对象是不会被释放的。
为此我做了个实验:
public class Test {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static ThreadLocal<FBar> TREAD_LOCALS = new ThreadLocal<>();
@Test
public void testThreadLocal() throws InterruptedException {
FBar value = new FBar("1");
new Thread(() -> {
TREAD_LOCALS.set(value);
}).start();
new Thread(() -> {
TREAD_LOCALS.set(new FBar("2"));
}).start();
Thread.sleep(1000);
System.gc();
}
static class FBar {
private final String s;
private int[] re = new int[16];
public FBar(String s) {
this.s = s;
}
@Override
protected void finalize() throws Throwable {
// 这算是Java的析构函数,在垃圾回收前会调用该方法
System.out.println("i be remove!" + s);
}
}
}
输出结果:
i be remove!2
对,只有2被打印了。这印证了类头部的注释。因为这里的1始终在主线程中有引用。
结论:
即使没有显示调用remove方法,当线程死亡时,ThreadLocal的本地变量依然可以被回收。除非本地变量还有其他引用指向它。
回到书中的例子(这里偷懒没贴出来书中的例子,有需要的给我留言,我再码上来。。。),扩展一下String相关的例子。
众所周知,String是比较特殊的,他有常量池,而我们平时直接使用的"xxx",是会直接入池的。而new StringBuilder(“a”).append(“b”).toString();则不然,不会自动入池,除非你显示调用.intern()方法,否则"ab"不会入池,直到你显示调用它。并且,如果你先使用了"ab",再用toString().intern()返回的就是"ab"的对象地址。而入池之后,关于这块的垃圾回收,就不在这里拓展了。
回到问题
ThreadLocal内存泄漏是伪命题?
关于这个问题,对,也不对。
- 不对。因为从上面的分析,当线程长期存活时,确实存在内存泄漏风险。
- 对。只要线程死亡,就会被回收。此外,从使用的角度,ThreadLocal的get/set调用到底层的Entry时,遇到null都会进行清理,即使扩容,也会先清理一波。出现key为null的概率也会降低。
这里多说一下,为什么是概率降低,而不是100%?因为清理工作建立在hash冲突的时候,才会触发清理所有的key为null的entry。(感兴趣的同学可以看下set方法的清理源码和方法注释。)也就是说,只要访问的Entry存在且key不为空,就不会清理。但换个角度一想,既然创建Map时都能申请到table数组空间,晚些清理,好像也不是多大的问题。
java.lang.ThreadLocal.ThreadLocalMap#replaceStaleEntry
码农的日常
我们很多时候,实际上都是在跟线程池打交道。例如:tomcat线程池。换而言之,线程通常会被重复利用,而不会消亡。这就可能使得上述发生内存泄漏成为可能。
除此之外,还有另外一重风险:重复使用了上一次的线程本地变量!(在gc清理掉上次使用的变量之前,而本次又访问了该副本)因为上一次的线程使用把threadLocal设置了,但是该线程再次被使用时,使用了一样的值!
因此,在C+V的日常,当使用到threadlocal时,要多注意一下,显示调用一下remove方法,避免这些问题!
后记
本来想看下多数据源切换的,下次吧。。。晚安。
边栏推荐
- 剖析SGI STL空间配置器(_S_refill内存块填充函数)
- TreeSet解析
- 编程界的“躲猫猫”比赛 | 每日趣闻
- 如何使用 Jmeter 进行抢购、秒杀等场景下,进行高并发?
- XP电源维修fleXPower电源X7-2J2J2P-120018系列详解
- EMC过不了?都是PCB工程师的锅?
- Reflection tricks can boost your performance by N times
- Devops和低代码的故事:螳螂捕蝉,黄雀在后
- Network/Information Security Top Journal and Related Journals Conference
- TreeSet parsing
猜你喜欢
Unreal Engine Graphic Notes: could not be compiled. Try rebuilding from source manually. Problem solving
How to run dist file on local computer
C language classic practice questions (3) - "Hanoi Tower (Hanoi)"
积分专题笔记-与路径无关条件
MySQL中使用IN 不会走索引分析以及解决办法
ACL 2022 | Introduce angular margin to construct comparative learning objectives and enhance text semantic discrimination ability
An article to understand service governance in distributed development
MySQL Explain usage and parameter detailed explanation
浅论各种调试接口(JTAG、SWD、RDI、Jlink、Ulink、STlink)的区别
Kotlin value class - value class
随机推荐
延迟队列MQ
Use the R language to read the csv file into a data frame, and then view the properties of each column.
HCIP - MPLS VPN experiment
深入浅出零钱兑换问题——背包问题的套壳
【HMS core】【FAQ】HMS Toolkit典型问题合集1
The FPGA based protocol 2: the I2C read and write E squared PROM
电源完整性的去耦和层间耦合电容
C语言经典练习题(3)——“汉诺塔(Hanoi)“
HashSet和LinkedHashSet
嘉为鲸翼·多云管理平台荣获信通院可信云技术服务最佳实践
代码随想录笔记_哈希_202 快乐数
[Fun BLDC series with zero basics] Taking GD32F30x as an example, the timer related functions are explained in detail
树莓派_烧写Raspberry官方镜像系统
剑指offer 48:最长不重复子串
MySQL中使用IN 不会走索引分析以及解决办法
HCIP --- MPLS VPN实验
微软 SQL 服务器被黑,带宽遭到破坏
Farthest Point Sampling - D-FPS vs F-FPS
剖析SGI STL空间配置器(空间配置器的重要性和重要成员及函数)
读书笔记:《这才是心理学:看穿伪心理学的本质(第10版)》