从名字上看,『ThreadLocal』可能会给你一种本地线程的概念印象,可能会让你联想到它是一个特殊的线程。java
但实际上,『ThreadLocal』却营造了一种「线程本地变量」的概念,也就是说,同一个变量在每一个线程的内部,都有一份副本,且相互之间具备不一样的取值。git
这样的设计具备怎样的应用场景呢?是怎么样的一种设计原理呢?程序员
别急,本篇就来详细的探讨探讨它。github
上面咱们粗略的介绍了「什么是 ThreadLocal ?」的这个问题,下面咱们来看看它的一个基本使用是什么样的,以及设计出来旨在解决什么问题等相关内容。数组
咱们先看这么一段程序:安全
函数 A 调用了函数 B,接着调用了函数 C、D,这么深层次的调用体系在真实的业务场景下是很常见的。微信
可是假如我如今要对函数 D 中要打印的字符串进行动态的传入,那你是否是得修改每个方法的形参列表,增长一个形参位,接着在函数 A 中的调用上传入一个参数过来?多线程
这太繁琐了,咱们使用 ThreadLocal 就能够简单解决这种「需求变动」的问题:并发
这一连串函数的调用必然是同一个线程调用的,那么咱们只要在最开头存储下一个变量,不管当前线程调用了多少层函数,这个局部变量一直都存在。函数
这是 ThreadLocal 的一种使用场景,但有点低估它的价值了,ThreadLocal 最经常使用的使用场景是,在多线程并发情境下避免一些因为共享变量竞争访问致使的并发问题。
咱们来看看广为你们诟病的 SimpleDateFormat,周所周知,这是个多线程不安全的类,咱们再次回顾下之前的内容:
SimpleDateFormat 是一个用于格式化日期和字符串的工具类,主要有两个核心方法,format 和 parse,前者用于将一个日期转换成指定格式的字符串,后者用于将一个指定格式的字符串转换成一个日期对象。
可是,这两个方法都不是线程安全的,format 方法倒还好,最多致使传入的 Date 格式化成错误的值,而 parse 将直接致使多种异常。缘由很简单,他们公用了同一个局部变量。
format 方法的第一个行就是将传入的 Date 对象保存到父类 DateFormat 的字段 calendar 上,而后会在后面逻辑中读取这个 Date 实例并完成转换字符串的逻辑。
可是彻底有可能在你设置完日期时间后,其余线程也执行 format 方法并覆盖了你的日期时间 calendar 中的值,这样你后续的转换字符串的动做基于的日期已经再也不是传入的日期对象了,致使的最终结果就是错误将别人的日期 Date 转换成字符串并返回了。
不信,你看这么一段代码:
执行后,我给你找一个错误的数据打印日志:
明显的是构造的上一个线程传入的 Date 参数,也就是在格式化的过程当中被别的线程覆盖了本身传入的 Date 致使的错误的格式化数据。
parse 方法的线程不安全就不带你们重现了,它更严重,由于方法内部会执行一个 clear 操做清空 calendar 字段保存的值,而且仍是非线程安全式的清空,会致使某些其余线程发生转换异常的,具体的你们能够本身去看。
而咱们简单的使用 ThreadLocal 就能够解决上述 format 的线程不安全问题:
ThreadLocal 的 set 方法将致使每一个线程的内部都持有一个 SimpleDateFormat 的实例,本身用本身的,也就不存在由于共享变量而致使的数据一致性问题了。
以上,咱们介绍了 ThreadLocal 的两种不一样的使用场景,其中第二种更加的常见一点,下面咱们来看原理。
ThreadLocal 在使用上仍是很简单的,可是其内部实现以及与各个线程的关联仍是有些绕的,接下来咱们深刻去看看。
基本字段属性
除了 threadLocalHashCode 是一个常量,每当建立一个新的 ThreadLocal 实例的时候就会根据 nextHashCode 和 HASH_INCREMENT 去计算初始的赋值。
由于 nextHashCode 是静态的,是类共享的,因此,每建立一个 ThreadLocal 实例,它的 threadLocalHashCode 是前一个实例的基础上加固定常量 0x61c88647。
这个值经换算是一个斐波那契数,每次增量该常量能够分散 hash 值的分布,减小后续在 map 中定位保存数据时产生冲突。
内部类 ThreadLocalMap
ThreadLocalMap 的内部实现是很相似 HashMap 的内部实现的,若是你分析过 HashMap,这一块会容易理解不少,下面咱们看其中重要的几个字段:
首先,Entry 这个类是 ThreadLocalMap 中定义的内部类,很简单,保存了两个主要内容,一个是 ThreadLocal 的局部变量,一个是 Object 类型的 value 值。
INITIAL_CAPACITY 指定了 table 的初始化容量,或者说是默认的数组初始化长度。
size 指定了 table 中实际有效的 Entry 数量。
threshold 是一个阈值的概念抽象,当 table 的 size 达到了这个阈值,就会触发一个动态扩容动做,扩容 table。
因此,对于 ThreadLocal 的一个不太恰当的理解是,它只是一个封装了 hashCode 的 key,这个 key 决定了咱们的 value 该保存在 ThreadLocalMap 内部 table 的哪一个位置。
这一点也在它的构造函数中也可见一斑:
这个 i 就是当前 Entry 要保存在 table 上的具体索引,它是如何计算的?
就是用咱们的 key(ThreadLocal 实例)内部保存的 hashcode 取余 table 容量计算而来。
threshold 会被设置为 table 容量的三分之二。
至于其中的 set、get 方法咱们待会分析,至此 ThreadLocal 中已经不剩下什么重要的东西了,虽然 ThreadLocalMap 是 ThreadLocal 的内部类,可是与 ThreadLocal 所表现出来的语义并无很密切的关系,可能为了某些安全性吧,将 ThreadLocalMap 定义为了 ThreadLocal 的静态内部类。
set、get方法原理
介绍以前,咱们先看 Thread 类中的一个字段:
Thread 类中持有了两个 ThreadLocalMap 实例,两个实例稍有区别,inheritableThreadLocals 相比于 threadLocals 来讲具备更大的特殊性。
区别在于,若是父线程(即建立本身的那个线程)使用了 inheritableThreadLocals 存储线程本地变量,那么本线程的建立过程当中也会使用 inheritableThreadLocals 进行本地变量的存储而且将父线程中全部的本地变量进行一份拷贝,填充到本身的 inheritableThreadLocals 中。
具体怎么实现的你们能够自行去查看,jdk 中从新定义了一个 InheritableThreadLocal 类,继承的 ThreadLocal 并重写了其中的 getMap 方法,致使你外部的 get 操做会转而返回 inheritableThreadLocals 而再也不是 threadLocals。
如今咱们来看 ThreadLocal 的 set 方法:
set 方法仍是很简单的,获取当前线程内部的 ThreadLocalMap 实例,若是不是空的就往里面增长一条记录,反之先初始化一个 map 再增长一条记录进去。
核心仍是在 ThreadLocalMap 的 set 方法:
这个方法的大致逻辑以下:
可能有些细心的人会疑问,为何整个方法内没看到一行处理并发的同步语句?
有这样的疑问,你可能尚未彻底理解 ThreadLocal 的设计思路,ThreadLocalMap 已是线程的私有领地了,别的线程是不可能访问的到的,又何来同步问题?
get 方法:
既然存是用的 ThreadLocal 实例做为 key,取天然也是根据该实例进行 get 了,并不难理解。
到这里,关于 ThreadLocal 基本的类结构体系、与 Thread 的关联关系,以及核心的 set、get 方法逻辑实现咱们都予以了分析,不知道你理解的怎样了呢?欢迎你和我交流!
在这以前,咱们关注一个问题,不少人对 ThreadLocal 的一个误解,以为他是不安全的,会产生『内存泄漏』的问题,咱们一块儿来看看是否是这样。
首先,ThreadLocal 确实是存在『内存泄漏』这个内存隐患的,可是一大堆人把源头指向 Entry 这个节点类。
很明显,咱们 Entry 将 key 存储为『弱引用』,什么是弱引用这里再也不赘述了,而将 value 存储为『强引用』,因而他们的内存结构就是这样的(盗了张图):
咱们的 ThreadLocal 实例被建立在堆中,方法栈中存在一个对它的强引用,咱们的 Entry 实例中存在一个对他的弱引用。
重点来了,有人就认为,一旦我在主程序中丢失了对该实例的强引用,或是赋空了该实例,那么 GC 会无视该实例存在着一个弱引用,而直接回收了该资源,以致于你永远没法访问到该 Entry 实例的 value 属性且没法回收它,因此致使的内存泄漏。
看起来是有道理,可是不使用弱引用就没有内存泄漏了吗?
你换成强引用,会致使整个 Entry 实例都是无用数据,更大的内存泄漏。反而使用弱引用后,当你调用 get 方法的时候,会因为 key 为 null,执行清除逻辑,将 Entry 实例赋 null,最后由 GC 回收该内存资源。
但这始终不能解决 ThreadLocal 的内存泄漏问题,建议的作法是,当某个本地变量不用的时候,手动的调用 remove 方法进行移除。期待 jdk 能更新 ThreadLocal 的实现,代码层解决这个问题。