前言
本文采用图解方式来带大伙通关 ThreadLocal ,同时我希望大伙有一定的 J V M 基础,这样食用起来会更香。
相信大伙对 ThreadLocal 并不陌生,工作中常用,同时也是面试的高频题,但是大部分人对 ThreadLocal 可能只停留在 「就是一个线程本地变量,Map结构」的表象上,没有深入的去思考过,所以这篇文章会让大伙通透 ThreadLocal ,给大伙在工作上带来帮助,也让面试有更多的谈资。
内容大纲
Java对象引用级别
在聊 ThreadLocal前,先做前置知识铺垫,谈谈Java对象引用级别。
为了使程序能更灵活地控制对象的生命周期,从 JDK 1.2 版本开始,JDK把对象的 引用级别 由 高到低 分为 强引用、软引用、弱引用、虚引用 四种级别
强引用(StrongReference)
强引用是我们最常见的对象,它属于不可回收的资源,垃圾回收器(后面简称G C)绝对不会回收它,即使是内存不足,J V M宁愿抛出 OutOfMemoryError 异常,使程序终止,也不会来回收强引用对象
软引用(SoftReference)
如果对象是软引用,那么它的性质属于可有可无,因为 内存空间充足 的情况下,G C不会回收它,但是 内存空间一旦紧张,G C发现它 仅有软引用 就会回收该对象,所以 软引用 适合作为 内存敏感的缓存对象
如果对象仅被 SoftReference 所引用,它才是真正意义上的软引用对象,因为一个对象可以在多处被引用,所以被 SoftReference 所引用的对象,不一定是真正意义上的软引用对象,它有可能在其他处被强引用了
弱引用(WeakReference)
弱引用对象 相对于 软引用对象 具有 更短暂的生命周期,只要 G C 扫描到它,并仅有弱引用,不管内存空间是否充足,都会回收它,不过 G C 是一个 优先级很低 的线程,因此不一定 会很快 发现那些只仅有弱引用的对象
如果对象仅被 WeakReference 所引用,它才是真正意义上的弱引用对象,因为一个对象可以在多处被引用,所以被 WeakReference 所引用的对象,不一定是真正意义上的弱引用对象,它有可能在其他处被强引用了
虚引用(PhantomReference)
顾名思义,虚引用形同虚设,与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅有虚引用,那它就和没有任何引用一样,在任何时候都可能被 G C 回收
此时大伙会问,感觉 虚引用 和 弱引用 没什么区别,其实它们的区别挺大的,SoftReference(软引用)、WeakReference(弱引用)、PhantomReference(虚引用)都提供 get()方法 获取真实对象引用,SoftReference、WeakReference引用的对象,没被回收时,使用 get()方法 能获取到真实对象引用,但是PhantomReference使用 get()方法,永远返回null,也就是说「无法通过虚引用来获取对象的真实引用」
❝
小结一下,Java 中 SoftReference、WeakReference、PhantomReference对象,可以理解为 对象引用级别包装类,在项目中使用对应的包装类,赋予对象引用级别,另外在虚引用图中,出现了ReferenceQueue(引用队列),引用队列是配合 对象引用级别包装类(SoftReference、WeakReference、PhantomReference)使用,当 对象引用级别包装类 所指向的对象 被垃圾回收后,该 对象引用级别包装类 则被追加到引用队列,因此可以通过引用队列做GC相关的统计或额外数据清理等操作。
”
ThreadLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫线程本地存储,其实意思差不多。大伙都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。
ThreadLocal是什么
Thread类声明了一个成员变量threadLocals,threadLocals 才是真正的线程本地变量,因此每个 Thread对象 都有自己的线程本地变量,所以线程本地变量天生具备线程隔离性(线程安全),这时大伙肯定会有疑问,threadLocals与ThreadLocal之间有什么联系?
从上图可以看到 Thread.threadLocals成员变量类 是 ThreadLocal.ThreadLocalMap,即 Thread.threadLocals成员变量类 是 ThreadLocal 提供的 内部类ThreadLocalMap,因此 Thread 线程本地变量的创建、新增、获取、删除实现核心,必然是围绕 Thread.threadLocals,所以开发者也是围绕 Thread.threadLocals 实现功能,为了后续重复使用,还会对这些代码实现进行封装复用,而 ThreadLocal 就是线程本地变量工具类,由 J D K 提供,线程本地变量操作的功能都已经实现好了,开箱即用,造福广大开发人员。
ThreadLocal常用的方法
set:为当前线程设置变量,当前ThreadLocal作为索引
get:获取当前线程变量,当前ThreadLocal作为索引
initialValue(钩子方法需要子类实现):赖加载形式初始化线程本地变量,执行get时,发现线程本地变量为null,就会执行initialValue的内容
remove:清空当前线程的ThreadLocal索引与映射的元素
从上图中可以看出,一个 Threa可以拥有 多个ThreadLocal键值对(存储在ThreadLocalMap结构),又因为 ThreadLocalMap 依赖当前Thread,Thread对象销毁时 ThreadLocalMap 也会跟着一并销毁,所以ThreadLocalMap的生命周期与Thread绑定,又因为线程隔离,天生的线程安全(ThreadLocalMap == Thread.threadLocals)。
说了这么多ThreadLocal的概念,可以总结出「本地线程变量的作用域,属于当前线程整个范围,一个线程可以跨越多个方法使用本地线程变量」,当你希望 某些变量 在某 Thread 的多个方法中共享 并保证线程安全,那就大胆的使用ThreadLocal(ps:一定要想清楚,是某个变量被Thread生命周期内多个方法共享,还是多个Thread共享这个变量!)。
ThreadLocal源码
上图是针对User类实现的线程本地变量代码,方法也不多,分别是initialValue、get、set、remove,接下来这些方法源码进行解析
ThreadLocalMap结构
为了后面的源码解析体验更好,有必要介绍下出ThreadLocalMap结构,ThreadLocalMap顾名思义,它是 Map 结构,但是本文主要内容不是Map,所以千言万语不如上一图,快速过一下这块内容
通过上图,相信大伙对 ThreadLocalMap 结构已经非常清晰,同时还发现 ThreadLocal 被打上弱引用标签(WeakReference),为什么ThreadLocal会被弱引用?这块疑惑后面会给大伙安排的明明白白,最后上一张 ThreadLocalMap 源码图
get方法
步骤如下
获取当前线程
获取当前线程的本地变量
线程本地变量没有被创建,执行setInitialValue方法进行初始化,并返回value值
线程本地变量存在,ThreadLocal计算成索引从 本地线程变量 获取Entry,如果Entry为null,执行setInitialValue方法进行初始化,并返回value值,否则通过Entry获取value返回
initialValue方法
步骤如下
通过get方法触发
执行初始化,获取到value
获取当前线程
获取当前线程本地变量
如果当前线程本地变量存在 ,ThreadLocal计算成索引设置映射的value,否则创建线程本地变量再做后续的设置操作
返回value值
set方法
步骤如下
获取当前线程
获取线程本地变量
本地变量不为空,当前ThreadLocal为索引设置映射的value,否则创建线程本地变量再做后续的设置操作
remove方法
步骤如下
获取Entry数组
当前ThreadLocal计算出索引
根据索引获取Entry元素(若是第一次没有命中,就循环直到null)
清除Entry元素
❝
源码十分简单,核心就三样,ThreadLocal线程本地变量工具类(同时作为索引)、Entry基本元素(弱引用包装类ThreadLocal与value),Entry数组容器,到这里流程很清晰了,ThreadLocal计算出数组索引,用ThreadLocal与value构建出Entry元素,最终放入Entry容器中,相信大伙都能写出来。
”
为何采用弱引用
为什么Entry中对ThreadLocal使用弱引用?这里反问一句,如果使用强引用,会发生什么事情?
上面的代码作用仅仅只是是为了让大伙去理解为什么使用弱引用,一般开发中不会出现这样的代码(真出现了,这程序员怕是要去祭天),回到正题,对上图的代码进行解析,首先ThreadContextTest类 拥有 私有的静态变量ThreadLocal,且ThreadContextTest类 禁止实例化,接着执行静态方法run触发静态块为ThreadLocal 设置一个 User变量 并消除 ThreadLocal 强引用,此时当前线程的本地变量拥有了Entry元素。
问题来了,要如何获取到 Entry 元素,按正常流程,首先通过ThreadContextTest.ThreadLoca执行get方法,get方法会使用当前ThreadLoca计算出索引,最终获取到Entry元素,可是现在拿不到ThreadContextTest.ThreadLoca,就像下图一样
不知道key是什么,如何去获取你映射的value,就像拿不到ThreadContextTest.ThreadLoca一样的道理,使用map结构却不能通过key去获取value值,这设计也不合理 同时 key与value值因为是强引用还会导致 G C 无法回收,造成内存溢出,所以针对这块 J D K 做了优化,对Entry中的ThreadLocal使用弱引用,当 G C 发现它仅有弱引用的时候,会进行回收。
正常使用场景的ThreadLoca
源码图
内存图
2B使用场景的ThreadLoca
源码图
内存图
remove背后的意义
还没结束,上面还了小尾巴,大伙都知道Entry中对ThreadLocal使用弱引用,但value是强引用,如果出现了上面提到的2B使用场景,value值无法清理,最终内存溢出。
其实value作为强引用设计合理,如果使用软或弱引用,就出大问题了,程序跑着跑着,突然get到了一个null,估计都得骂娘了,所以针对value,J D K提供了一个remove方法,使开发人员可以选择手动清理整个Entry元素,防止内存溢出。
此时新问题出现了,还记的之前说过的,线程本地变量的生命周期 与 线程绑定,一般线程的生命周期比较短,线程结束时,线程本地变量自然就销毁了,软引用与remove会不会有点多余了?
业务瞬息万变,大部分情况来看线程的生命周期比较短,但也业务场景导致线程的生命周期较长,甚至可能线程无限循环执行,这些是你没办法预料到的,数量一旦上来很容易内存溢出。
个人建议使用完之后及时清理ThreadLocal,理由如下
生命周期较长的线程场景
无限循环线程的场景
线程池场景(因为线程池可以复用线程,而且公司使用的框架可能会实现定制化线程池,你不能保证他会在线程池内帮你remove)
唠叨唠叨
先祝大伙新年快乐,万事如意!!!博主两周肝一篇,虽然周期有点长,但是质量有保证,码文不易,如果觉得本文对您有帮助,欢迎分享给你的朋友,也给阿星点个「点赞+收藏」,这对阿星非常重要,谢谢您们,给各位小姐姐小哥哥们抱拳了,我们下次见!
关于我
公众号 : 「程序猿阿星」 专注技术原理、源码,通过图解方式输出技术,这里将会分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章,期待你的关注。
本文转自 https://mp.weixin.qq.com/s/qMep17llDFWHq_S0IWh6ZA,如有侵权,请联系删除。