一. 简介
提醒篇幅较大需耐心。
简介来自ThreadLocal类注释
ThreadLocal类提供了线程局部 (thread-local) 变量。这些变量与普通变量不同,每个线程都可以通过其 get 或 set方法来访问自己的独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
下面是类注释中给出的一个列子:
以下类生成对每个线程唯一的局部标识符。 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。
1 | import java.util.concurrent.atomic.AtomicInteger; |
输出结果 :01234
只要线程是活动的并且 ThreadLocal 实例是可访问的,每个线程都会保持对其线程局部变量副本的隐式引用;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
二. 整体认识
UML类图
ThreadLocal中的嵌套内部类ThreadLocalMap,这个类本质上是一个map,和HashMap之类的实现相似,依然是key-value的形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用(之后会详细说到)。
还是说ThreadLocalMap(下面是很大篇幅的阅读其源码,毕竟了解清楚了ThreadLocalMap的来龙去脉,ThreadLocal基本也就差不多了),在ThreadLocal中并没有对于ThreadLocalMap的引用,是的,ThreadLocalMap的引用在Thread类中,代码如下。每个线程在向ThreadLocal里塞值的时候,其实都是向自己所持有的ThreadLocalMap里塞入数据;读的时候同理,首先从自己线程中取出自己持有的ThreadLocalMap,然后再根据ThreadLocal引用作为key取出value,基于以上描述,ThreadLocal实现了变量的线程隔离(当然,毕竟变量其实都是从自己当前线程实例中取出来的)。
1 | public |
原理图
根据理解,画出ThreadLocal原理图如下:
- 首先,主线程定义的两个ThreadLocal变量,和两个子线程——线程A和线程B。
- 线程A和线程B分别持有一个ThreadLocalMap用于保存自己独立的副本,主线程的ThreadLocal中封装了get()和set()之类的方法。
- 在线程A和线程B中调用ThreadLocal的set方法,会首先通过getMap(Thread.currentThread)获得线程A或者是线程B持有的ThreadLocalMap,在调用map.put()方法,并将ThreadLocal作为key。
- get()方法和set()方法原理类似,也是先获取当前调用线程的ThreadLocalMap,再从map中获取value,并将ThreadLocal作为key。
三. ThreadLocalMap源码分析
下面一步一步介绍ThreadLocalMap源码分析的相关源码,在分析ThreadLocalMap的同时,也会介绍与ThreadLocalMap关联的ThreadLocal中的方法(这样分析完ThreadLocalMap,ThreadLocal基本就搞定了),可能有些需要前后结合才能真正理解。
成员变量
1 | /** |
各个值的含义已经在注释里面说了,就不再一一解释。
存储结构——Entry
1 | /** |
Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。
关于WeakReference可以参考我之前的博客,关于Java中的WeakReferencea。
ThreadLocalMap的set()方法和Hash映射
要了解ThreadLocalMap中Hash映射的方式,首先从ThreadLocal的set()方法入手,逐层深入。
ThreadLocal中的set()
先看看ThreadLocal中set()源码。
1 | public void set(T value) { |
- 代码很简单,获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。
- 如果获取到的map实例不为空,调用map.set()方法,否则调用构造函数 ThreadLocal.ThreadLocalMap(this, firstValue)实例化map。
可以看出来线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。下面来看看构造函数ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
。
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
主要说一下计算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
。
- 关于
& (INITIAL_CAPACITY - 1)
,这是取模的一种方式,对于2的幂作为模数取模,用此代替%(2^n)
。至于为什么可以这样这里不过多解释,原理很简单。 - 关于
firstKey.threadLocalHashCode
:
1 | private final int threadLocalHashCode = nextHashCode(); |
定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,关于这个值和斐波那契散列
有关,其原理这里不再深究,感兴趣可自行搜索,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table
中。
在了解了上面的源码后,终于能进入正题了,下面开始进入ThreadLocalMap中的set()。
ThreadLocalMap中的set()
ThreadLocalMap使用线性探测法
来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把table看成一个环形数组
。
先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:
1 | /**java |
ThreadLocalMap的set()及其set()相关代码如下:
1 | private void set(ThreadLocal<?> key, Object value) { |
ThreadLocalMap中的getEntry()及其相关
同样的对于ThreadLocalMap中的getEntry()也从ThreadLocal的get()方法入手。
ThreadLocal中的get()
1 | public T get() { |
ThreadLocalMap中的getEntry()
1 | private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) { |
ThreadLocalMap中的remove()
1 | private void remove(ThreadLocal<?> key) { |
remove()在有上面了解后可以说极为简单了,就是找到对应的table[],调用weakrefrence的clear()清除引用,然后再调用expungeStaleEntry()进行清除。
四. 总结
分析完ThreadLocalMap,ThreadLocal的神秘面纱也就揭开了,所以不再赘述。