保姆级教学,22张图揭开ThreadLocal

前言

图解方式来通关ThreadLocal,同时希望你们有一定的JVM 基础,这样食用起来会更香。

相信大伙对 ThreadLocal 并不陌生,工作中常用,同时也是面试高频题,但是大部分人对 ThreadLocal 的理解可能只是「线程的本地变量,Map结构」,看完本文让大伙真正理解ThreadLocal,给大伙工作带来帮助,也让面试有更多的谈资。

内容大纲

保姆级教学,22张图揭开ThreadLocal

Java对象引用级别

在聊 ThreadLocal 前,先做前置知识铺垫,谈谈Java对象引用级别。

为了使程序能更灵活地控制对象生命周期,从 JDK1.2 版本开始,JDK把对象的引用级别由高到低分为强引用、软引用、弱引用、虚引用四种级别。

保姆级教学,22张图揭开ThreadLocal

强引用 StrongReference

强引用是我们最常见的对象,它属于不可回收资源,垃圾回收器(后面简称G C)绝对不会回收它,即使是内存不足,J V M宁愿抛出 OutOfMemoryErrorM 异常,使程序终止,也不会来回收强引用对象。

保姆级教学,22张图揭开ThreadLocal

软引用 SoftReference

如果对象是软引用,那它的性质属于可有可无,因为内存空间充足的情况下,G C不会回收它,但是内存空间紧张,G C发现它仅有软引用,就会回收该对象,所以软引用对象适合作为内存敏感的缓存对象。

保姆级教学,22张图揭开ThreadLocal

只有对象仅被 SoftReference 引用,它才是软引用级别对象,因为对象可以在多处被引用,所以 SoftReference 引用的对象,它可能在其他处被强引用了。

弱引用 WeakReference

弱引用对象相对软引用对象具有更短暂的生命周期,只要 G C 发现它仅有弱引用,不管内存空间是否充足,都会回收它,不过 G C 是一个优先级很低的线程,因此不一定会很快发现那些仅有弱引用的对象。

保姆级教学,22张图揭开ThreadLocal

只有对象仅被 WeakReference 引用,它才是弱引用级别对象,因为对象可以在多处被引用,所以 WeakReference 引用的对象,它可能在其他处被强引用了。

虚引用 PhantomReference

顾名思义,虚引用形同虚设,与其他几种引用不同,虚引用不会决定对象的生命周期。

如果一个对象仅有虚引用,那它就和没有任何引用一样,任何时候都可能被 G C 回收。

保姆级教学,22张图揭开ThreadLocal

读到这里会不会感觉虚引用和弱引用没区别?它们的区别如下

  • SoftReference、WeakReference引用的对象没被回收时,可以使用get方法获取真实对象地址
  • PhantomReference使用get方法永远返回null

简单说就是「无法通过虚引用来获取对象的真实地址」

小结

Java中SoftReference、WeakReference、PhantomReference,可以理解为对象引用级别包装类,在项目中使用对应的包装类,赋予对象引用级别。

虚引用图中,出现了ReferenceQueue(引用队列),引用队列是配合对象引用级别包装类(SoftReference、WeakReference、PhantomReference)使用,当对象引用级别包装类所指向的对象,被垃圾回收后,该对象引用级别包装类被追加到引用队列,因此可以通过引用队列做 G C 相关统计或额外数据清理等操作。

保姆级教学,22张图揭开ThreadLocal

ThreadLocal

ThreadLocal很多地方叫线程本地变量,也有些地方叫线程本地存储,其实意思差不多。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

保姆级教学,22张图揭开ThreadLocal

ThreadLocal是什么

Thread类声明了成员变量threadLocals,threadLocals才是真正的线程本地变量,因此每个 Thread 都有自己的线程本地变量,所以线程本地变量拥有线程隔离特性,也就是天生的线程安全。

保姆级教学,22张图揭开ThreadLocal

从上图可以看到 threadLocals 成员变量类是 ThreadLocal.ThreadLocalMap,即是 ThreadLocal 提供的内部类,因此 Thread 线程本地变量的创建、新增、获取、删除实现核心,必然是围绕 threadLocals,所以开发者也是围绕 threadLocals 实现功能,为了后续重复使用,还会对代码实现进行封装复用,而 ThreadLocal 就是线程本地变量工具类,由 J D K 提供,线程本地变量的功能都已经实现好了,开箱即用,造福广大开发人员。

ThreadLocal常用的方法

  • set:为当前线程设置变量,当前ThreadLocal作为索引
  • get:获取当前线程变量,当前ThreadLocal作为索引
  • initialValue(钩子方法需要子类实现):赖加载形式初始化线程本地变量,执行get时,发现线程本地变量为null,就会执行initialValue的内容
  • remove:清空当前线程的ThreadLocal索引与映射的元素

保姆级教学,22张图揭开ThreadLocal

一个 Threa可以拥有多个 ThreadLocal键值对(存储在ThreadLocalMap结构),又因为 ThreadLocalMap 依赖当前Thread,Thread销毁时 ThreadLocalMap 也会随之销毁,所以 ThreadLocalMap 的生命周期与 Thread 绑定。

保姆级教学,22张图揭开ThreadLocal

现在总结出「本地线程变量的作用域,属于当前线程整个范围,一个线程可以跨越多个方法使用本地线程变量」,当你希望某些变量在某 Thread 的多个方法*享 并保证线程安全,那就大胆的使用ThreadLocal(ps:一定要想清楚,是某个变量被Thread生命周期内多个方法共享,还是多个Thread共享这个变量!)。

ThreadLocal源码

先来看看User类实现的线程本地变量代码

保姆级教学,22张图揭开ThreadLocal

方法也不多,分别是initialValue、get、set、remove,接下来这些方法源码进行解析。

ThreadLocalMap结构

为了后面的源码解析体验更好,有必要介绍下ThreadLocalMap,顾名思义,它是 Map 结构,但是本文主要内容不是Map,所以上一图,快速过一下这块内容。

保姆级教学,22张图揭开ThreadLocal

通过上图,相信大伙对 ThreadLocalMap 结构已经非常清晰,不知有没有细心的小伙伴发现 ThreadLocal 竟被弱引用持有?

为什么ThreadLocal会被弱引用?这块疑惑后面会给大伙安排的明明白白,最后上一张 ThreadLocalMap 源码图。

保姆级教学,22张图揭开ThreadLocal

get 获取变量

保姆级教学,22张图揭开ThreadLocal

步骤如下

  1. 获取当前线程
  2. 获取当前线程的本地变量
  3. 线程本地变量没有被创建,执行setInitialValue方法进行初始化,并返回value值
  4. 线程本地变量存在,ThreadLocal计算成索引从 本地线程变量 获取Entry,如果Entry为null,执行setInitialValue方法进行初始化,并返回value值,否则通过Entry获取value返回

initialValue方法

保姆级教学,22张图揭开ThreadLocal

步骤如下

  1. 通过get方法触发
  2. 执行初始化,获取到value
  3. 获取当前线程
  4. 获取当前线程本地变量
  5. 如果当前线程本地变量存在 ,ThreadLocal计算成索引设置映射的value,否则创建线程本地变量再做后续的设置操作
  6. 返回value值

set 设置变量

保姆级教学,22张图揭开ThreadLocal

步骤如下

  1. 获取当前线程
  2. 获取线程本地变量
  3. 本地变量不为空,当前ThreadLocal为索引设置映射的value,否则创建线程本地变量再做后续的设置操作

remove 清除变量

保姆级教学,22张图揭开ThreadLocal

步骤如下

  1. 获取Entry数组
  2. 当前ThreadLocal计算出索引
  3. 根据索引获取Entry元素(若是第一次没有命中,就循环直到null)
  4. 清除Entry元素

小结

源码十分简单,核心就三样ThreadLocal线程本地变量工具类(同时作为索引)、Entry基本元素(由弱引用包装类ThreadLocal与value组成),Entry数组容器,到这里流程很清晰了,ThreadLocal计算出数组索引,用 ThreadLocal 与 value 构建出 Entry 元素,最终放入 Entry 容器中,相信大伙都能写出来。

保姆级教学,22张图揭开ThreadLocal

为何采用弱引用

为什么 Entry 中对 ThreadLocal 使用弱引用?反问一句,如果使用强引用,会发生什么事情?

保姆级教学,22张图揭开ThreadLocal

上图的代码作用仅仅只是是为了让大伙去理解为什么使用弱引用,一般开发中不会出现这样的代码(真出现了,这程序员怕是要拉去祭天)。

回到正题,我们快速对代码进行解析,首先 ThreadContextTest 持有私有的静态变量 ThreadLocal,且 ThreadContextTest 禁止实例化,接着执行静态方法 run 触发静态块为 ThreadLocal 设置User变量 并消除 ThreadLocal 强引用,此时当前线程的本地变量拥有了Entry元素。

问题来了,要如何获取到 Entry 元素,按正常流程,ThreadLocal执行 get 方法,get会使用当前 ThreadLocal 计算出索引,最终获取到Entry元素,可是现在的问题如同下图。

保姆级教学,22张图揭开ThreadLocal

我们不知道 key 是什么,如何去获取映射的value,同样的道理,都没有入口去获取到ThreadContextTest.ThreadLoca,自然没办法获取映射的Entry元素。

设计中采用Map结构存储数据,却不能通过key去获取value,这设计明显不合理,又因key、value值是强引用,导致 G C 无法回收,造成内存溢出。

所以针对这种不合理的设计场景 J D K 做了优化,对 Entry 中的 ThreadLocal 使用弱引用,当 G C 发现它仅有弱引用的时候,会进行回收。

保姆级教学,22张图揭开ThreadLocal

remove背后的意义

还没结束,上面留了个小尾巴,大伙都知道 Entry 中对 ThreadLocal 使用弱引用,但value是强引用,如果出现上面提到的不合理场景,value值无法清理,最终内存溢出。

其实value作为强引用设计属于合理,如果用软或弱引用,就出大问题了,程序跑着跑着突然get到了一个null,估计都得骂娘了,所以为解决内存溢出问题 J D K提供remove方法,使开发人员可以选择手动清理整个Entry元素,防止内存溢出。

还记的之前说过吗?线程本地变量的生命周期与线程绑定,一般线程的生命周期比较短,线程结束时,线程本地变量自然就销毁了,软引用与 remove 会不会有点多余了?

业务瞬息万变,大部分情况来说线程的生命周期比较短,但也业务场景会导致线程的生命周期较长,甚至可能线程无限循环执行,这些是你没办法预料到的,数量一旦上来很容易内存溢出,所以个人建议使用完之后及时清理ThreadLocal,理由如下

  • 生命周期较长的线程场景
  • 无限循环线程的场景
  • 线程池场景(因为线程池可以复用线程,而且公司使用的框架可能会定制化线程池,你不能保证他会在线程池内帮你remove)
唠叨唠叨

先祝大伙新年快乐,万事如意!!!博主两周肝一篇,虽然周期有点长,但是质量有保证,码文不易,如果觉得本文对您有帮助,欢迎分享给你的朋友,也给阿星点个「点赞+收藏」,这对阿星非常重要,谢谢您们,给各位小姐姐小哥哥们抱拳了,我们下次见!

关于我

公众号 : 「程序猿阿星」 专注技术原理、源码,通过图解方式输出技术,这里将会分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章,期待你的关注。

上一篇:单例模式在多线程中的使用情况


下一篇:面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》