ThreadLocal源码分析-黄金分割数的使用
前提
最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal
(实际上用的是InheritableThreadLocal
)用于在子线程获取父线程中共享的变量。问题是解决了,但是后来发现对ThreadLocal
的理解不够深入,于是顺便把它的源码阅读理解了一遍。在谈到ThreadLocal
之前先买个关子,先谈谈黄金分割数。本文在阅读ThreadLocal
源码的时候是使用JDK8(1.8.0_181)。
黄金分割数与斐波那契数列
首先复习一下斐波那契数列,下面的推导过程来自某搜索引擎的wiki:
- 斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
- 通项公式:假设F(n)为该数列的第n项(n ∈ N*),那么这句话可以写成如下形式:F(n) = F(n-1) + F(n-2)。
有趣的是,这样一个完全是自然数的数列,通项公式却是用无理数来表达的。而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近0.618(或者说后一项与前一项的比值小数部分越来越逼近0.618),而这个值0.618就被称为黄金分割数。证明过程如下:
黄金分割数的准确值为(根号5 - 1)/2,约等于0.618。
黄金分割数的应用
黄金分割数被广泛使用在美术、摄影等艺术领域,因为它具有严格的比例性、艺术性、和谐性,蕴藏着丰富的美学价值,能够激发人的美感。当然,这些不是本文研究的方向,我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:
public static void main(String[] args) throws Exception { |
通过一个线段图理解一下:
也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。**而ThreadLocal中的哈希魔数正是1640531527(十六进制为0x61c88647)**。为什么要使用0x61c88647作为哈希魔数?这里提前说一下ThreadLocal
在ThreadLocalMap
(ThreadLocal
在ThreadLocalMap
以Key的形式存在)中的哈希求Key下标的规则:
哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)
其中,i为ThreadLocal
实例的个数,这里的HASH_INCREMENT就是哈希魔数0x61c88647,length为ThreadLocalMap
中可容纳的Entry
(K-V结构)的个数(或者称为容量)。在ThreadLocal
中的内部类ThreadLocalMap
的初始化容量为16,扩容后总是2的幂次方,因此我们可以写个Demo模拟整个哈希的过程:
public class Main { |
上面的例子中,我们分别模拟了ThreadLocalMap
容量为4,16,32的情况下,不触发扩容,并且分别”放入”4,16,32个元素到容器中,输出结果如下:
3 2 1 0 |
每组的元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列。实际上,这个并不是偶然,其实整个哈希算法可以转换为多项式证明:证明(x - y) * HASH_INCREMENT != 2^n * (n m)
,在x != y,n != m
,HASH_INCREMENT为奇数的情况下恒成立,具体证明可以自行完成。HASH_INCREMENT赋值为0x61c88647的API文档注释如下:
连续生成的哈希码之间的差异(增量值),将隐式顺序线程本地id转换为几乎最佳分布的乘法哈希值,这些不同的哈希值最终生成一个2的幂次方的哈希表。
ThreadLocal是什么
下面引用ThreadLocal
的API注释:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)
稍微翻译一下:ThreadLocal
提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal
实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal
实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
ThreadLocal
由Java界的两个大师级的作者编写,Josh Bloch和Doug Lea。Josh Bloch是JDK5语言增强、Java集合(Collection)框架的创办人以及《Effective Java》系列的作者。Doug Lea是JUC(java.util.concurrent
)包的作者,Java并发编程的泰斗。所以,ThreadLocal
的源码十分值得学习。
ThreadLocal的原理
ThreadLocal
虽然叫线程本地(局部)变量,但是实际上它并不存放任何的信息,可以这样理解:它是线程(Thread
)操作ThreadLocalMap
中存放的变量的桥梁。它主要提供了初始化、set()
、get()
、remove()
几个方法。这样说可能有点抽象,下面画个图说明一下在线程中使用ThreadLocal
实例的set()
和get()
方法的简单流程图。
假设我们有如下的代码,主线程的线程名字是main(也有可能不是main):
public class Main { |
线程实例和ThreadLocal
实例的关系如下:
上面只描述了单线程的情况并且因为是主线程忽略了Thread t = new Thread()
这一步,如果有多个线程会稍微复杂一些,但是原理是不变的,ThreadLocal
实例总是通过Thread.currentThread()
获取到当前操作线程实例,然后去操作线程实例中的ThreadLocalMap
类型的成员变量,因此它是一个桥梁,本身不具备存储功能。
ThreadLocal源码分析
对于ThreadLocal
的源码,我们需要重点关注set()
、get()
、remove()
几个方法。
ThreadLocal的内部属性
//获取下一个ThreadLocal实例的哈希魔数 |
这里需要注意一点,threadLocalHashCode是一个final的属性,而原子计数器变量nextHashCode和生成下一个哈希魔数的方法nextHashCode()
是静态变量和静态方法,静态变量只会初始化一次。换而言之,每新建一个ThreadLocal
实例,它内部的threadLocalHashCode就会增加0x61c88647。举个例子:
//t1中的threadLocalHashCode变量为0x61c88647 |
threadLocalHashCode是下面的ThreadLocalMap
结构中使用的哈希算法的核心变量,对于每个ThreadLocal
实例,它的threadLocalHashCode是唯一的。
内部类ThreadLocalMap的基本结构和源码分析
ThreadLocal
内部类ThreadLocalMap
使用了默认修饰符,也就是包(包私有)可访问的。ThreadLocalMap
内部定义了一个静态类Entry
。我们重点看下ThreadLocalMap
的源码,先看成员和结构部分:
/** |
这里注意到十分重要的一点:**ThreadLocalMap$Entry
是WeakReference
(弱引用),并且键值Key为ThreadLocal<?>
实例本身,这里使用了无限定的泛型通配符**。
接着看ThreadLocalMap
的构造函数:
// 构造ThreadLocal时候使用,对应ThreadLocal的实例方法void createMap(Thread t, T firstValue) |
这里注意一下,ThreadLocal
的set()
方法调用的时候会懒初始化一个ThreadLocalMap
并且放入第一个元素。而ThreadLocalMap
的私有构造是提供给静态方法ThreadLocal#createInheritedMap()
使用的。
接着看ThreadLocalMap
提供给ThreadLocal
使用的一些实例方法:
// 如果Key在哈希表中找不到哈希槽的时候会调用此方法 |
简单来说,ThreadLocalMap
是ThreadLocal
真正的数据存储容器,实际上ThreadLocal
数据操作的复杂部分的所有逻辑都在ThreadLocalMap
中进行,而ThreadLocalMap
实例是Thread
的成员变量,在ThreadLocal#set()
方法首次调用的时候设置到当前执行的线程实例中。如果在同一个线程中使用多个ThreadLocal
实例,实际上,每个ThreadLocal
实例对应的是ThreadLocalMap
的哈希表中的一个哈希槽。举个例子,在主函数主线程中使用多个ThreadLocal
实例:
public class ThreadLocalMain { |
实际上,主线程的threadLocals属性中的哈希表中一般不止我们上面定义的三个ThreadLocal
,因为加载主线程的时候还有可能在其他地方使用到ThreadLocal
,笔者某次Debug的结果如下:
用PPT画图简化一下:
上图threadLocalHashCode属性一行的表是为了标出每个Entry的哈希槽的哈希值,实际上,threadLocalHashCode是ThreadLocal@XXXX
中的一个属性,这是很显然的,本来threadLocalHashCode就是ThreadLocal
的一个成员变量。
上面只是简单粗略对ThreadLocalMap
的源码进行了流水账的分析,下文会作一些详细的图,说明一下ThreadLocal
和ThreadLocalMap
中的一些核心操作的过程。
ThreadLocal的创建
从ThreadLocal
的构造函数来看,ThreadLocal
实例的构造并不会做任何操作,只是为了得到一个ThreadLocal
的泛型实例,后续可以把它作为ThreadLocalMap$Entry
的键:
// 注意threadLocalHashCode在每个新`ThreadLocal`实例的构造同时已经确定了 |
注意threadLocalHashCode在每个新ThreadLocal
实例的构造同时已经确定了,这个值也是Entry哈希表的哈希槽绑定的哈希值。
TreadLocal的set方法
ThreadLocal
中set()
方法的源码如下:
public void set(T value) { |
上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量threadLocals。步骤是:
- 获取当前运行线程的实例。
- 通过线程实例获取线程实例成员threadLocals(
ThreadLocalMap
),如果为null,则创建一个新的ThreadLocalMap
实例赋值到threadLocals。 - 通过threadLocals设置值value,如果原来的哈希槽已经存在值,则进行覆盖。
TreadLocal的get方法
ThreadLocal
中get()
方法的源码如下:
public T get() { |
initialValue()
方法默认返回null,如果ThreadLocal
实例没有使用过set()
方法直接使用get()
方法,那么ThreadLocalMap
中的此ThreadLocal
为Key的项会把值设置为initialValue()
方法的返回值。如果想改变这个逻辑可以对initialValue()
方法进行覆盖。
TreadLocal的remove方法
ThreadLocal
中remove()
方法的源码如下:
public void remove() { |
ThreadLocal.ThreadLocalMap的初始化
我们可以关注一下java.lang.Thread
类里面的变量:
public class Thread implements Runnable { |
也就是,ThreadLocal
需要存放和获取的数据实际上绑定在Thread
实例的成员变量threadLocals中,并且是ThreadLocal#set()
方法调用的时候才进行懒加载的,可以结合上一节的内容理解一下,这里不展开。
什么情况下ThreadLocal的使用会导致内存泄漏
其实ThreadLocal
本身不存放任何的数据,而ThreadLocal
中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread
对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal
实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal
,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal
实例的时候,JVM的GC会回收ThreadLocalMap
中的这些Key,此时,ThreadLocalMap
中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal
中get()
、set()
、remove()
这些方法中都存在清理ThreadLocalMap
实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:
- 1、大量地(静态)初始化
ThreadLocal
实例,初始化之后不再调用get()
、set()
、remove()
方法。 - 2、初始化了大量的
ThreadLocal
,这些ThreadLocal
中存放了容量大的Value,并且使用了这些ThreadLocal
实例的线程一直处于活跃的状态。
ThreadLocal
中一个设计亮点是ThreadLocalMap
中的Entry
结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap
中的所有数据都是与Thread
的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal
在下一次调用get()
、set()
、remove()
方法就可以删除那些ThreadLocalMap
中Key为null的值,起到了惰性删除释放内存的作用。
其实ThreadLocal
在设置内部类ThreadLocal.ThreadLocalMap
中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry
类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>
。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:
public class ThreadLocalMain { |
这种情况下,TL_1这个ThreadLocal
在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap
实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference
)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal
未回收之前的值。这些被”孤立”的哈希槽Entry就是前面说到的要惰性删除的哈希槽。
ThreadLocal的最佳实践
其实ThreadLocal
的最佳实践很简单:
- 每次使用完
ThreadLocal
实例,都调用它的remove()
方法,清除Entry
中的数据。
调用remove()
方法最佳时机是线程运行结束之前的finally
代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。
父子线程数据传递InheritableThreadLocal
留待下一篇文章编写,因为InheritableThreadLocal
只能通过父子线(1->1)程传递变量,线程池里面的线程有可能是多个父线程共享的(也就是1个父线程提交的任务有可能由线程池中的多个子线程执行),因此有可能出现问题。阿里为了解决这个问题编写过一个框架-transmittable-thread-local,解决了父线程和线程池中线程的变量传递问题。
小结
ThreadLocal
线程本地变量是线程实例传递和存储共享变量的桥梁,真正的共享变量还是存放在线程实例本身的属性中。ThreadLocal
里面的基本逻辑并不复杂,但是一旦涉及到性能影响、内存回收(弱引用)和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。
(本文完 e-a-20190217 c-7-d)