当前位置: 首页 > news >正文

ThreadLocal详解

☆* o(≧▽≦)o *☆嗨~我是小奥🍹
📄📄📄个人博客:小奥的博客
📄📄📄CSDN:个人CSDN
📙📙📙Github:传送门
📅📅📅面经分享(牛客主页):传送门
🍹文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
📜 如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️

文章目录

  • ThreadLocal详解
    • 1. 基本介绍
    • 2. Thread、ThreadLocal、ThreadLocalMap的关系
    • 3. 实现原理
      • ① initialValue()
      • ② get()
      • ③ set(T value)
      • ④ remove
    • 4. ThreadLocalMap
      • ① Entry
      • ② Set()
      • ③ getEntry
      • ④ rehash()
      • ⑤ remove()
    • 5. 内存泄漏

ThreadLocal详解

1. 基本介绍

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例时(通过get和set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段。使用它的目的是希望将状态(例如用户ID或者事务ID)与线程关联起来。这些变量分配在堆内的TLAB中。

ThreadLocal的使用非常简单,只需要在每个线程中调用set()方法来存储数据,然后在需要的时候调用get()方法来获取数据。在多线程环境下,每个线程都拥有自己的ThreadLocal实例,因此可以独立地存储和访问自己的数据,从而避免了线程安全问题。

ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的。

ThreadLoacl作用:

  • 线程并发:应用在多线程并发下的场景
  • 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度
  • 线程隔离:每个线程的变量都是独立的,不会相互影响

对比 synchronized:

synchronizedThreadLocal
原理同步机制采用以时间换空间的方式,
只提供了一份变量,让不同的线程排队访问
ThreadLocal 采用以空间换时间的方式,
为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

常用方法:

方法描述
get()返回当前线程的此线程局部变量副本中的值
set(T value)将当前线程的此线程局部变量的副本设置为指定的值
remove()删除此线程局部变量的当前线程的值
initialValue()返回此线程局部变量的当前线程的“初始值”
withInitial(Supplier<? extends S>)创建线程局部变量

阿里巴巴规范约定:必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用try-finally块进行回收。

2. Thread、ThreadLocal、ThreadLocalMap的关系

  • 每个Thread线程内部都有一个Map(ThreadLocal.ThreadLocalMap)
  • Map里面存储的是ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map中获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,其他的线程并不能获取当前线程的副本值,所以形成了副本隔离。

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。

在这里插入图片描述

3. 实现原理

① initialValue()

返回该线程局部变量的初始值。

  • 延迟调用的方法,在执行get方法时才调用
  • 该方法缺省(默认)实现直接返回null
  • 如果想要一个初始值,可以重写此方法,该方法是一个protected的方法,就是为了让子类覆盖而设计的
	protected T initialValue() {return null;}

② get()

获取当前线程与当前ThreadLocal对象相关联的线程局部变量。

	public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果此map存在if (map != null) {// 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 eThreadLocalMap.Entry e = map.getEntry(this);// 对 e 进行判空 if (e != null) {// 获取存储实体 e 对应的 value值T result = (T)e.value;return result;}}/*有两种情况有执行当前代码第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */// 初始化当前线程与当前 threadLocal 对象相关联的 valuereturn setInitialValue();}
	private T setInitialValue() {// 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 nullT value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 判断 map 是否初始化过if (map != null)// 存在则调用 map.set 设置此实体 entry,value 是默认的值map.set(this, value);else// 调用 createMap 进行 ThreadLocalMap 对象的初始化中createMap(t, value);// 返回线程与当前 threadLocal 关联的局部变量return value;}

③ set(T value)

	public void set(T value) {// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的 ThreadLocalMap 对象ThreadLocalMap map = getMap(t);// 判断 map 是否存在if (map != null)// 调用 threadLocalMap.set 方法进行重写或者添加map.set(this, value);else// map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量createMap(t, value);}
	// 获取当前线程 Thread 对应维护的 ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 创建当前线程Thread对应维护的ThreadLocalMap void createMap(Thread t, T firstValue) {// 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据t.threadLocals = new ThreadLocalMap(this, firstValue);}

④ remove

	public void remove() {// 获取当前线程对象中维护的 ThreadLocalMap 对象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体m.remove(this);}

4. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;
// 存放数据的table,数组长度必须是2的整次幂。
private Entry[] table;
// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值
private int size = 0;
// 进行扩容的阈值,表使用量大于它的时候进行扩容。
private int threshold;

① Entry

  • Entry继承WeakReference,key是弱引用,目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
  • Entry限制只能使用ThreadLocal作为key,key为null(entry.get() == null ) 意味着不再被引用,entry也可以从table中清除。
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {// this.referent = referent = key;super(k);value = v;}
}

② Set()

添加数据,ThreadLocalMap使用线性探测法来解决哈希冲突。

  • 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍。

    假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组

  • 线性探测法会出现堆积问题,可以采取平方探测法。

  • 在探测过程中ThreadLocal会复用key为null的脏Entry对象,并进行垃圾清理,防止出现内存泄漏。

private void set(ThreadLocal<?> key, Object value) {// 获取散列表ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 哈希寻址int i = key.threadLocalHashCode & (len-1);// 使用线性探测法向后查找元素,碰到 entry 为空时停止探测for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 获取当前元素 keyThreadLocal<?> k = e.get();// ThreadLocal 对应的 key 存在,【直接覆盖之前的值】if (k == key) {e.value = value;return;}// 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】// key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】if (k == null) {// 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】// 这个方法还进行了垃圾清理动作,防止内存泄漏replaceStaleEntry(key, value, i);return;}}// 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entrytab[i] = new Entry(key, value);// 数量 + 1int sz = ++size;// 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)// 扩容rehash();
}
// 获取【环形数组】的下一个索引
private static int nextIndex(int i, int len) {// 索引越界后从 0 开始继续获取return ((i + 1 < len) ? i + 1 : 0);
}
// 在指定位置插入指定的数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {// 获取散列表Entry[] tab = table;int len = tab.length;Entry e;// 探测式清理的开始下标,默认从当前 staleSlot 开始int slotToExpunge = staleSlot;// 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值// 【保证在一个区间段内,从最前面的过期数据开始清理】for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {// 获取当前节点的 keyThreadLocal<?> k = e.get();// 条件成立说明是【替换逻辑】if (k == key) {e.value = value;// 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致// 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry// 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置,tab[i] = tab[staleSlot];tab[staleSlot] = e;// 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是if (slotToExpunge == staleSlot)slotToExpunge = i;// 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据if (k == null && slotToExpunge == staleSlot)// 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据slotToExpunge = i;}// 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】// 删除原有的数据引用,防止内存泄露tab[staleSlot].value = null;// staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】tab[staleSlot] = new Entry(key, value);// 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在这里插入图片描述

private static int prevIndex(int i, int len) {// 形成一个环绕式的访问,头索引越界后置为尾索引return ((i - 1 >= 0) ? i - 1 : len - 1);
}

③ getEntry

ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e。

private Entry getEntry(ThreadLocal<?> key) {// 哈希寻址int i = key.threadLocalHashCode & (table.length - 1);// 访问散列表中指定指定位置的 slot Entry e = table[i];// 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回if (e != null && e.get() == key)return e;else// 进行线性探测return getEntryAfterMiss(key, i, e);
}
// 线性探测寻址
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {// 获取散列表Entry[] tab = table;int len = tab.length;// 开始遍历,碰到 slot == null 的情况,搜索结束while (e != null) {// 获取当前 slot 中 entry 对象的 keyThreadLocal<?> k = e.get();// 条件成立说明找到了,直接返回if (k == key)return e;if (k == null)// 过期数据,【探测式过期数据回收】expungeStaleEntry(i);else// 更新 index 继续向后走i = nextIndex(i, len);// 获取下一个槽位中的 entrye = tab[i];}// 说明当前区段没有找到相应数据// 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数return null;
}

④ rehash()

触发一次全量清理,如果数组长度大于等于长度的1/2,则进行resize。

private void rehash() {// 清楚当前散列表内的【所有】过期的数据expungeStaleEntries();// threshold = len * 2 / 3,就是 2/3 * (1 - 1/4)if (size >= threshold - threshold / 4)resize();
}
private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;// 【遍历所有的槽位,清理过期数据】for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}
}

Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC。

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新数组的长度是老数组的二倍int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];// 统计新table中的entry数量int count = 0;// 遍历老表,进行【数据迁移】for (int j = 0; j < oldLen; ++j) {// 访问老表的指定位置的 entryEntry e = oldTab[j];// 条件成立说明老表中该位置有数据,可能是过期数据也可能不是if (e != null) {ThreadLocal<?> k = e.get();// 过期数据if (k == null) {e.value = null; // Help the GC} else {// 非过期数据,在新表中进行哈希寻址int h = k.threadLocalHashCode & (newLen - 1);// 【线程探测】while (newTab[h] != null)h = nextIndex(h, newLen);// 将数据存放到新表合适的 slot 中newTab[h] = e;count++;}}}// 设置下一次触发扩容的指标:threshold = len * 2 / 3;setThreshold(newLen);size = count;// 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用table = newTab;
}

⑤ remove()

删除Entry。

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)]) {// 找到了对应的 keyif (e.get() == key) {// 设置 key 为 nulle.clear();// 探测式清理expungeStaleEntry(i);return;}}
}

5. 内存泄漏

(1)内存泄漏产生:ThreadLocal 内存泄漏的原因通常是因为ThreadLocalMap 是 Thread的一个属性, ThreadLocal 的生命周期和线程的生命周期一样长。而Entry将ThreadLocal作为key,值作为value对象,它继承自WearReference,并且在构造函数中调用了super()方法,所以ThreadLocal对象的key是一个弱引用,而value是一个强引用。

当GC回收ThreadLocal时,弱引用的key会被回收,但是强引用的value不会被回收,就会造成内存泄漏。

主要有两个原因:

  • 没有手动删除这个Entry
  • 当前线程依然继续运行

关于第一点,只要在使用完ThreadLocal,手动调用其remove方法删除对应的Entry,就能避免内存泄漏。

关于第二点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期和线程一样。如果ThreadLocal变量被回收,那么当前线程的ThreadLocal变量副本所指向的key就是null,即Entry的结构为(null,value),那么这个Entry对应的value永远无法被访问到,而value还存在线程的强引用,只有在线程退出以后,value的强引用才会断开。

(2)如何解决内存泄漏

  • 及时清除 ThreadLocal 变量副本:在使用完 ThreadLocal 后,及时调用 remove() 方法删除对应线程的变量副本,避免造成内存泄漏。可以考虑使用 try-finally 语句块来确保在方法结束时一定会调用 remove() 方法。
  • 使用弱引用的 ThreadLocal 实例:可以使用 WeakReference 来包装 ThreadLocal 实例,这样 ThreadLocal 实例会被视为弱引用,当没有强引用指向 ThreadLocal 实例时,ThreadLocal 实例就会被垃圾回收。
  • 使用线程池或者线程复用机制:在一些情况下,可以使用线程池或者线程复用机制来避免频繁地创建和销毁线程,从而降低 ThreadLocal 内存泄漏的风险。

(3)为什么不把Key设置为强引用

如果当ThreadLocalMap的key为强引用,当垃圾回收时,由于ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除的话,那么ThreadLocal不会被回收,导致Entry内存泄漏。

通过使用 WeakReference 来包装 ThreadLocal 实例,可以让 ThreadLocal 实例变为弱引用,当没有其他强引用指向 ThreadLocal 实例时,ThreadLocal 实例就会被垃圾回收。因此,使用 WeakReference 可以避免 ThreadLocal 实例的内存泄漏。

由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set()、getEntry()、remove()方法的时候会通过线性探测法堆key进行判断,如果key为null(即ThreadLocal为null)则会对Entry进行垃圾回收。所以使用弱引用比强引用多一层保障,就算不调用 remove,也有机会进行 GC。

相关文章:

  • apipost和curl收不到服务器响应的HTTP/1.1 404 Not Found
  • 探索IOC和DI:解密Spring框架中的依赖注入魔法
  • 基于51单片机智能电子秤
  • 《vtk9 book》 官方web版 第2章 - 面向对象设计
  • jenkins安装配置,使用Docker发布maven项目全过程记录(1)
  • git checkout和git switch的区别
  • 微信小程序(十五)自定义导航栏
  • 定向减免!函数计算让轻量 ETL 数据加工更简单,更省钱
  • 那些年与指针的爱恨情仇(一)---- 指针本质及其相关性质用法
  • C# 只读文件删除提示失败,给文件修改属性
  • 【论文笔记】《Learning Deconvolution Network for Semantic Segmentation》
  • YOLOv8加入AIFI模块,附带项目源码链接
  • JSON-handle工具安装及使用
  • 2024年可能会用到的几个地图可视化模板
  • 五、详细设计说明书(软件工程)
  • 【JavaScript】通过闭包创建具有私有属性的实例对象
  • 2017 前端面试准备 - 收藏集 - 掘金
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • node.js
  • Python 使用 Tornado 框架实现 WebHook 自动部署 Git 项目
  • Python十分钟制作属于你自己的个性logo
  • select2 取值 遍历 设置默认值
  • springboot_database项目介绍
  • vue学习系列(二)vue-cli
  • 测试如何在敏捷团队中工作?
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 基于webpack 的 vue 多页架构
  • 将回调地狱按在地上摩擦的Promise
  • 力扣(LeetCode)22
  • 马上搞懂 GeoJSON
  • 职业生涯 一个六年开发经验的女程序员的心声。
  • nb
  • 翻译 | The Principles of OOD 面向对象设计原则
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • # centos7下FFmpeg环境部署记录
  • # Java NIO(一)FileChannel
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #单片机(TB6600驱动42步进电机)
  • $GOPATH/go.mod exists but should not goland
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (第8天)保姆级 PL/SQL Developer 安装与配置
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)memcache、redis缓存
  • .net core控制台应用程序初识
  • .NET Core引入性能分析引导优化
  • .Net IE10 _doPostBack 未定义
  • .NET/C# 使用 ConditionalWeakTable 附加字段(CLR 版本的附加属性,也可用用来当作弱引用字典 WeakDictionary)
  • .NET精简框架的“无法找到资源程序集”异常释疑
  • .NET文档生成工具ADB使用图文教程
  • [Android Pro] android 混淆文件project.properties和proguard-project.txt
  • [AS3]URLLoader+URLRequest+JPGEncoder实现BitmapData图片数据保存
  • [Assignment] C++1
  • [C++] Windows中字符串函数的种类