ThreadLocal源码解析

server/2025/1/31 20:29:27/

文章目录

  • 一、概述
  • 二、get()方法
  • 三、set()方法
  • 四、可能导致的内存泄漏问题
  • 五、remove
  • 六、思考:为什么要将ThreadLocalMap的value设置为强引用?


一、概述

  ThreadLocal是线程私有的,独立初始化的变量副本。存放在和线程进行绑定的ThreadLocalMap中。ThreadLocalMap的内部类Entry,是一个键值对的结构,key是ThreadLocal对象,value是某个变量在某个时刻的副本。并且Entry继承了WeakReference类,使其key(ThreadLocal对象)成为弱引用,如果未正确使用remove方法,可能会导致内存泄漏问题。
在这里插入图片描述构造entry对象时,key使用父类的方法,被包装成弱引用。

  ThreadLocalMapThreadLocal的一个静态内部类:
在这里插入图片描述  但是其初始化的操作,是绑定在每个线程中的,作为线程对象的属性,随着线程对象的加载而初始化。
在这里插入图片描述  并且ThreadLocalMap的内部,是一个entry键值对数组的形式。也有初始容量和扩容机制。这一点和HashMap类似,区别在于,处理Hash冲突时,HashMap使用的是拉链法,也就是对于Hash值相同的key,会形成一条链表乃至树化。而ThreadLocalMap使用的是开放定址法中的线性探测再散列,即某一个key计算出的Hash值,该位置已经有了元素,则会沿着数组下标依次向后寻找空位。
在这里插入图片描述在这里插入图片描述线性探测再散列
  ThreadLocal通常会作为类的属性,并且用static关键字修饰。原因在于ThreadLocal需要从属于某个类,而不是具体的实例。


二、get()方法

  在get方法中,主要做了几件事:

  1. 获取ThreadLocalMap。
  2. ThreadLocalMap不为空,则获取ThreadLocalMap的Entry 对象,并且对象不为空,就返回该Entry 对象的value。
  3. ThreadLocalMap为空,就执行初始化操作。
java">    public T get() {//获取当前线程对象(调用的是本地方法)Thread t = Thread.currentThread();//根据线程对象,获取到与该线程一一对应的ThreadLocalMap(ThreadLocalMap 是线程对象的属性) ThreadLocalMap map = getMap(t);//map第一次是为空的if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//执行初始化操作return setInitialValue();}

  执行初始化操作:

java">    private T setInitialValue() {//一、初始化value为nullT value = initialValue();//获取当前线程对象Thread t = Thread.currentThread();//二、用当前线程对象,获取ThreadLocalMap ThreadLocalMap map = getMap(t);//map不为空,就将当前的ThreadLocal对象作为key,null作为value,构造Entry对象。if (map != null)map.set(this, value);else//三、否则初始化mapcreateMap(t, value);//返回nullreturn value;}//一protected T initialValue() {return null;}//二ThreadLocalMap getMap(Thread t) {//ThreadLocal.ThreadLocalMap threadLocals = null;return t.threadLocals;}//三void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

三、set()方法

  set方法和get方法大同小异,也是根据当前线程获取ThreadLocalMap ,然后判空,执行set操作还是初始化操作。

java">    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)//一、向ThreadLocalMap 的entry中插入元素的操作。map.set(this, value);elsecreateMap(t, value);}

四、可能导致的内存泄漏问题

  先看一下这段代码,声明了一个线程池,以及LocalVariable 静态内部类,其中的成员变量是5M大的数组。并且还有一个ThreadLocal属性,将LocalVariable作为key包装成了弱引用。

java">public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024 * 1024 * 5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {SleepTools.ms(4000);for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {SleepTools.ms(500);
//
//                    LocalVariable localVariable = new LocalVariable();
//
//
//                    ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
//                    oom.threadLocalLV = new ThreadLocal<>();
//                    oom.threadLocalLV.set(new LocalVariable());
//
//                   oom.threadLocalLV.remove();System.out.println("use local varaible");}});SleepTools.ms(100);}System.out.println("pool execute over");}}

  如果没有加入ThreadLocal,而是仅仅在空跑,使用jvisualvm进行观察,看到的内存使用情况,是相对比较平稳的:
在这里插入图片描述  接着打开注释:

java">  ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();oom.threadLocalLV = new ThreadLocal<>();oom.threadLocalLV.set(new LocalVariable());

  发现在运行过程中,内存使用情况起伏明显,多次触发gc。
在这里插入图片描述  并且最终程序执行完成后,内存还处于较高的水平,也就是说明堆中还存在很多没有被回收的垃圾对象。
在这里插入图片描述  为什么和没有使用ThreadLocal之前,会有如此大的差距?原因在于,每次垃圾回收时,作为弱引用的Entry的key:ThreadLocal对象会被回收,但是其value没有被回收。(在JVM停止时统一销毁)。
在这里插入图片描述  而每个线程中都存在一个5M的强引用对象没有被回收。
  解决方式是,在ThreadLocal使用完成后,手动调用remove方法进行清除:

java">    public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

五、remove

  在remove方法中,主要完成了三件事:

  1. 获取哈希表。
  2. 计算索引,根据 key(即 ThreadLocal 对象)的哈希码,计算它在哈希表中的索引。
  3. 遍历表格中的链表查找匹配的条目。

  而expungeStaleEntry方法中,除了将value和entry的引用全部置空以外,还会继续向后扫描,将ThreadLocalMap中弱引用的key已经被回收的entry的value置空。(get和set方法中也有该实现)

java">        private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//将key的引用置空e.clear();//一expungeStaleEntry(i);return;}}}//一private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlot//将指定下标的value的引用置空tab[staleSlot].value = null;//将指定下标的entry置空tab[staleSlot] = null;//table的长度减少size--;//这部分代码会继续扫描从 staleSlot 后的条目。如果遇到 ThreadLocal 对象已经被回收(k == null),则清除该条目。//否则,重新计算哈希位置并尝试将条目移动到新的位置。//这里使用了一个 while 循环来处理条目的重新定位,确保哈希表在移除过期条目后依然保持正确。Entry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

六、思考:为什么要将ThreadLocalMap的value设置为强引用?

  线程本地存储的一个核心需求是,数据必须与线程的生命周期绑定,直到该线程结束或者显式移除该值。如果线程本地存储的 value 被弱引用,就无法保证它在使用时的可用性,可能会导致意外的回收和不可预期的行为。如果 ThreadLocalMap 的 value 被设置为弱引用,那么 ThreadLocalMap 中的条目就可能会在垃圾回收时被回收,因为 value 被弱引用(即没有强引用指向它)。这就可能导致在需要访问该值时,数据已经被清理掉。
  最主要的点在于,ThreadLocalMap 的生命周期跟 Thread 一样长。



http://www.ppmy.cn/server/163874.html

相关文章

【漫话机器学习系列】062.向前逐步选择法(Forward Stepwise Selection)

向前逐步选择法&#xff08;Forward Stepwise Selection&#xff09; 1. 什么是向前逐步选择法&#xff1f; 向前逐步选择法是一种特征选择&#xff08;Feature Selection&#xff09;算法&#xff0c;主要用于模型构建时&#xff0c;从一组候选特征中逐步选择对模型性能影响…

均方误差(MSE)揭秘:预测模型的“真面目”

前言 在这个充满数据的世界里,我们需要各种方式来衡量一个模型的表现,尤其在回归问题中,均方误差(MSE)是我们非常常见的“好朋友”。它就像一位忠诚的侦探,默默为我们揭示预测值与实际值之间的真相。今天,让我们一起进入均方误差的世界,看看它是如何用简单却有效的方式…

代码随想录day4

24.两两交换链表&#xff1a;注意虚拟头节点的使用 ListNode* swapPairs(ListNode* head) {ListNode* dummy new ListNode();dummy->next head;ListNode* current dummy;while(current->next ! nullptr && current->next->next ! nullptr){ListNode* t…

计算机网络之计算机网络协议、接口、服务等概念

一、计算机网络协议 定义&#xff1a; 计算机网络协议是实现计算机网络中不同计算机系统之间的通信所必须遵守的通信规则的集合。这些规则规定了数据如何在网络中传递、处理的格式、同步问题等。 要素&#xff1a; 语法&#xff1a;数据与控制信息的结构或格式。 语义&#xf…

图漾相机——Sample_V1示例程序

文章目录 1.SDK支持的平台类型1.1 Windows 平台1.2 Linux平台 2.SDK基本知识2.1 SDK目录结构2.2 设备组件简介2.3 设备组件属性2.4 设备的帧数据管理机制2.5 SDK中的坐标系变换 3.Sample_V1示例程序3.1 DeviceStorage3.2 DumpCalibInfo3.3 NetStatistic3.4 SimpleView_SaveLoad…

【C语言】static关键字的三种用法

【C语言】static关键字的三种用法 C语言中的static关键字是一个存储类说明符&#xff0c;它可以用来修饰变量和函数。static关键字的主要作用是控制变量或函数的生命周期和可见性。以下是static关键字的一些主要用法和含义&#xff1a; 局部静态变量&#xff1a; 当static修饰…

Git进阶之旅:.gitignore 文件

介绍&#xff1a; 在项目中&#xff0c;我们可能一起提交多个文件 git add -A&#xff1a;提交所有变化git add -u&#xff1a;提交被修改(modified) 和被删除文件(deleted) 文件&#xff0c;不包括新文件(new) git add .&#xff1a;提交新文件(new) 和被修改文件(modif…

Blazor-@bind

数据绑定 带有 value属性的标记都可以使用bind 绑定&#xff0c;<div>、<span>等非输入标记&#xff0c;无法使用bind 指令的&#xff0c;默认绑定了 onchange 事件&#xff0c;onchange 事件是指在输入框中输入内容之后&#xff0c;当失去焦点时执行。 page &qu…