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