ThreadLocal源码剖析

news/2025/3/11 6:30:59/

文章目录

  • 四种引用的概念
  • Entry的类定义
  • 弱引用和内存泄漏
    • 如果key使用强引用
    • 如果key使用弱引用
    • 为什么使用弱引用
  • hash冲突的解决
  • ThreadLocalMap中的set方法
  • 演示垃圾回收
    • get触发GC
    • set触发GC
  • ThreadLocal-内存清理
    • 探测式清理(ExpungeStaleEntry)
    • 启发式清理(cleanSomeSlots)
    • ReplaceStaleEntry
    • rehash(全局清理)
  • 学习资料链接:
    • 视频和文档资料1:
    • 视频和文档资料2:

ThreadLocal的set,get方法只是作为一个接口,实质上还是对Thread里面的ThreadLocalMap里面的进行操作
在这里插入图片描述

在这里插入图片描述
jdk1.8之后,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

Thread t = new Thread();
Map map = getMap(t);//根据线程获取他的threadLocalMap
map.set(new ThreadLocal<xxx>(),new Object());

在这里插入图片描述

四种引用的概念

在这里插入图片描述

Entry的类定义

/** Entry继承WeakReference,并且用ThreadLocal作为key.* 如果key为null(entry.get() == null),意味着key不再被引用,* 因此这时候entry也可以从table中清除。* entry.get()返回的是ThreadLocal类型的key*/
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

弱引用和内存泄漏

强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。

弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果key使用强引用

假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

​ 此时ThreadLocal的内存图(实线表示强引用)如下:
在这里插入图片描述
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

​ 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

​ 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

​ 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

如果key使用弱引用

在这里插入图片描述
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

​ 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

​ 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏

​ 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

其实不管是强引用还是弱引用,都会漏内存,只不过是多少的问题,只要忘记remove了,强引用是漏了kv,弱引用漏了v。只能说有点改善,但是不多

为什么使用弱引用

根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry

  2. 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

​ 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

​ 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
在这里插入图片描述

最主要啊,是弱引用会导致key为null,如果key为null了,就说明这个Entry过期了,就要进行垃圾回收了

hash冲突的解决

public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)//调用了ThreadLocalMap的set方法map.set(this, value);elsecreateMap(t, value);}ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {//调用了ThreadLocalMap的构造方法t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}

这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

(这里调用了ThreadLocalMap的set方法)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

(这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析ThreadLocalMap这两个方法。

 /** firstKey : 本ThreadLocal实例(this)* firstValue : 要保存的线程本地变量*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//计算索引(重点代码)int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//设置值table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size = 1;//设置阈值setThreshold(INITIAL_CAPACITY);}

构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。

a. 关于firstKey.threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用private static AtomicInteger nextHashCode =  new AtomicInteger();//特殊的hash值private static final int HASH_INCREMENT = 0x61c88647;

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。每多一个ThreadLocal,都递增一次HASH_INCREMENT

b. 关于& (INITIAL_CAPACITY - 1)

​ 计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。

ThreadLocalMapset_168">ThreadLocalMap中的set方法

private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;//计算索引(重点代码,刚才分析过了)int i = key.threadLocalHashCode & (len-1);/*** 使用线性探测法查找元素(重点代码)*/for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//ThreadLocal 对应的 key 存在,直接覆盖之前的值if (k == key) {e.value = value;return;}// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,// 当前数组中的 Entry 是一个陈旧(stale)的元素if (k == null) {//用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏replaceStaleEntry(key, value, i);return;}}//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。tab[i] = new Entry(key, value);int sz = ++size;/*** cleanSomeSlots用于清除那些e.get()==null的元素,* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。* 如果没有清除i往后任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 *  rehash(执行一次全表的扫描清理工作)*/if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}/*** 获取环形数组的下一个索引*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

代码执行流程:

  • A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,

  • B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,

  • C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,

  • D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

​ 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析ThreadLocalMap使用线性探测法来解决哈希冲突的。

​ 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

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

​ 按照上面的描述,可以把Entry[] table看成一个环形数组。

演示垃圾回收

在这里插入图片描述
在这里插入图片描述

get触发GC

get(c)触发GC,恰好是要获取1位置的value,↓↓↓↓
在这里插入图片描述
get(c)之后,原来key为null,value为1,现在key为c,value为null

在这里插入图片描述

set触发GC

在这里插入图片描述
清理完的效果
在这里插入图片描述
针对以上探讨的情况,我们推荐使用remove来清理内存,set,get的方式比较局限

ThreadLocal_258">ThreadLocal-内存清理

英文解释:
Expunge:清理,清除
Stale:过期的,失效的

探测式清理(ExpungeStaleEntry)

ExpungeStaleEntry返回下一个Entry为null的位置i
在这里插入图片描述

启发式清理(cleanSomeSlots)

Slot:槽,就是哈希槽的意思
在这里插入图片描述
cleanSomeSlot的调用背景是基于set
在这里插入图片描述

ReplaceStaleEntry

替换旧的无效节点
目的在于基于staleSlot位置,往前找,直到遇到第一个Entry为null的位置,往前找的过程中记录最前面过期Entry的位置slotToExpunge
往前找完了之后往StaleSlot后面找,直到遇到第一个Entry为null的位置,往后找的过程中清理所有过期Entry

在这里插入图片描述
replaceStaleEntry的背景是基于set的调用
在这里插入图片描述

rehash(全局清理)

在这里插入图片描述

学习资料链接:

视频和文档资料1:

通过网盘分享的文件:视频-由浅入深,全面解析ThreadLocal
链接: https://pan.baidu.com/s/1_CtLTXjBlQ1LlqySTmWpwg?pwd=atfs 提取码: atfs
–来自百度网盘超级会员v8的分享

通过网盘分享的文件:资料-ThreadLocal.zip
链接: https://pan.baidu.com/s/15Vz0WJymEtahX8FL1WNqnQ?pwd=pedv 提取码: pedv
–来自百度网盘超级会员v8的分享

【黑马程序员Java基础教程由浅入深全面解析threadlocal】 https://www.bilibili.com/video/BV1N741127FH/?p=12&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

【黑马Java面试八股文教程,大厂面试必会100题之Java并发之ThreadLocal】 https://www.bilibili.com/video/BV1gs4y137dL/?p=2&share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

视频和文档资料2:

【透彻理解ThreadLocal,最新最全的面试题及解答】 https://www.bilibili.com/video/BV1L89wYzEc2/?share_source=copy_web&vd_source=afbacdc02063c57e7a2ef256a4db9d2a

通过网盘分享的文件:带你透彻理解Threadlocal.pdf
链接: https://pan.baidu.com/s/1jNfmpwChXb_qtZqk_F7ybg?pwd=xwyp 提取码: xwyp
–来自百度网盘超级会员v8的分享


http://www.ppmy.cn/news/1578272.html

相关文章

图形编辑器基于Paper.js教程24:图像转gcode的重构,元素翻转,旋转

前段时间在雕刻图片时&#xff0c;旋转图片&#xff0c;翻转图片后&#xff0c;发现生成准确的gcode&#xff0c;虽然尺寸对&#xff0c;但是都是以没有旋转&#xff0c;没有翻转的图片进行生成的。后来思考了一下&#xff0c;发现这真是一个大bug&#xff0c;无论图片如何选择…

FreeRTOS第17篇:FreeRTOS链表实现细节05_MiniListItem_t:FreeRTOS内存优化

文/指尖动听知识库-星愿 文章为付费内容,商业行为,禁止私自转载及抄袭,违者必究!!! 文章专栏:深入FreeRTOS内核:从原理到实战的嵌入式开发指南 1 为什么需要迷你列表项? 在嵌入式系统中,内存资源极其宝贵。FreeRTOS为满足不同场景需求,设计了标准列表项(ListItem_…

C++后端服务器开发技术栈有哪些?有哪些资源或开源库拿来用?

一、 C后台服务器开发是一个涉及多方面技术选择的复杂领域&#xff0c;特别是在高性能、高并发的场景下。以下是C后台服务器开发的一种常见技术路线&#xff0c;涵盖了从基础到高级的技术栈。 1. 基础技术栈 C标准库 C11/C14/C17/C20&#xff1a;使用现代C特性&#xff0c;如…

Linux系统编程--线程同步

目录 一、前言 二、线程饥饿 三、线程同步 四、条件变量 1、cond 2、条件变量的使用 五、条件变量与互斥锁 一、前言 上篇文章我们讲解了线程互斥的概念&#xff0c;为了防止多个线程同时访问一份临界资源而出问题&#xff0c;我们引入了线程互斥&#xff0c;线程互斥其实…

前端知识点---前端里的接口

文章目录 1. 接口&#xff08;Interface&#xff09;作为对象的类型定义&#xff1a;① 接口是对象的模版, 类也是对象的模版 可以定义对象的属性跟类型1. 接口是对象的模板&#xff1a;2. 类是对象的模板 ②类可以被具体实现 可以new一个类, 接口只能用来定义类型 接口没法被具…

SQLiteStudio:一款免费跨平台的SQLite管理工具

SQLiteStudio 是一款专门用于管理和操作 SQLite 数据库的免费工具。它提供直观的图形化界面&#xff0c;简化了数据库的创建、编辑、查询和维护&#xff0c;适合数据库开发者和数据分析师使用。 功能特性 SQLiteStudio 提供的主要功能包括&#xff1a; 免费开源&#xff0c;可…

解决Node Electron下调用Python脚本输出中文乱码的问题

博主原博客地址&#xff1a;https://www.lisok.cn/Front-End/610.html 调用Pyinstaller打包后的可执行文件方式如下: import { promisify } from util import { exec } from child_process import { app } from electronasync handleVerifyZy(id) {const entity await this.f…

单片机项目复刻需要的准备工作

一、前言 复刻单片机的项目的时候&#xff0c;有些模块是需要焊接的。很多同学对焊接没有概念。 这里说一下做项目的基本工具。 比如&#xff1a;像这种模块&#xff0c;都需要自己焊接了排针才可以链接的。 二、基本模块 2.1 单排排针 一些模块买回来是没有焊接的&#x…