实现原理
每个线程读写ThreadLocal
是线程隔离的,互相之间不会影响。其原因就是在于Thread
类有一个ThreadLocal.ThreadLocalMap
类型的属性,也就是说每个线程有一个自己的ThreadLocalMap
,读写某个ThreadLocal
时都会获取当前线程以及当前线程的ThreadLocalMap
属性,对其进行读写,以此实现线程隔离。以下是ThreadLocal
的几个关键方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 将自己作为 key
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 将自己作为 key
if (e != null) {
"unchecked") (
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看出,ThreadLocal
本身并没有太多东西,它只是作为ThreadLocalMap
的key,核心源码其实都在ThreadLocalMap
中。
弱引用
在开始源码分析之前,需要先了解弱引用这个概念,因为在ThreadLocalMap
中ThreadLocal
并不是直接作为key的,而是使用的弱引用对象Entry
。在Java中存在四种引用,分别是强引用、软引用、弱引用和虚引用,它们的区别如下:
- 强引用:通常我们通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的,那么它就不被回收
- 软引用:软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些
- 虚引用:虚引用是Java中最弱的引用,通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收
之所以需要弱引用,是因为在类似HashMap
的结构中,如果存放了一个key为Product
对象且value为1
的节点,此时我们有一个变量product
指向了这个Product
对象,当我们不再需要这个对象时,如果直接将product
设为null
,Product
对象其实并不会被回收,因为通过HashMap
它还存在一条强引用链,如果我们想让它被垃圾收集器回收,就必须将其彻底从HashMap
中移除,让它不再存在任何强引用。如果上述过程我们不想自己手动去实现,而是想告诉垃圾收集器在只有HashMap
中的key引用着Product
对象的情况下,就可以回收相应的Product
对象了,那么就可以使用弱引用。
Java中的弱引用具体指的是java.lang.ref.WeakReference<T>
类,我们使用一个指向Product
对象的弱引用对象来作为HashMap
的key
,只需这样定义这个弱引用对象:1
2Product product = new Product(...);
WeakReference<Product> weakProduct = new WeakReference<>(product);
而如果要通过weakProduct
获取它所指向的Product
对象,我们只需要通过这行代码:Product product = weakProductA.get();
即可。WeakReference
的构造函数如下:1
2
3
4//创建一个指向给定对象的弱引用
WeakReference(T referent)
//创建一个指向给定对象并且登记到给定引用队列的弱引用
WeakReference(T referent, ReferenceQueue<? super T> q)
通过将原始对象包装成弱引用对象,当变量product
设为null
时,指向这个Product
对象的就只剩弱引用对象weakProduct
了,显然这时候相应的Product
对象是弱可达的,所以指向它的弱引用会被清除,这个Product
对象随即会被回收,指向它的弱引用对象会进入引用队列中,在引用队列中可以对这些被清除的弱引用对象进行统一管理。
源码分析
Entry节点
上面说过,ThreadLocalMap
并不是简单的使用ThreadLocal
作为key的,其实它内部存储着一个Entry
节点数组,而Entry
继承了弱引用类WeakReference
:1
2
3
4
5
6
7
8
9static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
当构造一个Entry
节点时,会先调用父类WeakReference
的构造函数将ThreadLocal
传入,并设置了一个类型为Object
的value
,用于存放ThreadLocal
对应的值。
这里之所以要使用弱引用Entry
节点而不是简单的key-value形式的节点,是因为如果简单的使用key-value形式会造成节点的生命周期与线程强绑定,只要线程存在,那么作为属性的ThreadLocalMap
也就存在,在不显式移除的情况下,key对象就依然被强引用着,没办法被回收。在这里通过使用弱引用节点,当我们将某个ThreadLocal
对象的强引用设为null
后,这个ThreadLocal
对象就只剩下弱引用了,之后会被GC回收掉,有效的避免了内存泄漏的问题。
成员变量
1 | // 初始容量默认为16 |
ThreadLocalMap
与HashMap
不同,它是使用的线性探测法而非拉链法解决碰撞冲突的,所以实际上Entry[]
数组在逻辑上是作为一个环形存在的。
1 | // 环形意义的下一个索引下标 |
构造函数
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
这个构造函数我们重点需要关注其中的threadLocalHashCode
,这是传入的ThreadLocal
对象的哈希值:1
2
3
4
5
6
7
8
9 private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static final int HASH_INCREMENT = 0x61c88647;
这个哈希值在对象创建时就会生成,每次都会累加0x61c88647
,通过这种方式使得与2的幂取模(实际是位运算)后均匀分布,也就提高了线性探测时的效率。
getEntry
getEntry()
方法会被ThreadLocal
的get()
方法直接调用,上面也说过,get()
方法内部就是先拿到当前线程的ThreadLocalMap
,然后将自己this
作为参数调用其getEntry()
方法。这里要提前说明一点的是,每个索引(slot)上的状态有三种:有效(ThreadLocal
未回收),失效(ThreadLocal
已回收),空(null
):
1 | private Entry getEntry(ThreadLocal<?> key) { |
总的来说,getEntry()
会经历以下几步:
- 根据传入的
ThreadLocal
的哈希值定位到某个索引下标 - 如果该下标对应的
entry
存在,且其中的ThreadLocal
和方法传入的ThreadLocal
相同,则直接命中返回 - 否则,调用
getEntryAfterMiss()
进行线性探测,过程中每次碰到失效的 slot,就调用expungeStaleEntry
进行段清理(清理并rehash,直到遇到null) - 遍历直到 null 都未命中 key,直接返回 null
set
1 | private void set(ThreadLocal<?> key, Object value) { |
set()
方法总体过程如下:
- 在遍历(也就是线性探测)遇到null之前,如果遇到了相同的key,则直接覆盖;如果遇到了失效的entry,则调用
replaceStaleEntry
,效果是最终一定会把key和value放在这个slot上,并且会尽可能地清理无效entry - 遍历过程既没遇到相同的key,也没遇到失效的entry,也就是当前索引上为null,则直接将key和value插在这个空slot上
- 如果插入后的
size
大于阈值,那么做一次全量清理,再根据调低的阈值决定是否需要扩容,扩容两倍(因为容量必须为2的幂)
remove
remove()
方法相对比较简单,只需要找到对应的key,然后将弱引用显式的断开,并做一次段清理即可。
1 | private void remove(ThreadLocal<?> key) { |
这里光做e.clear();
其实是不够的,因为value
此时还被强引用着,所以才需要进行段清理,将table[i] = null;
彻底断开强引用。
内存泄露
经过上面的分析我们已经清楚在每个Thread
中有一个ThreadLocalMap
,每个线程在对某个ThreadLocal
对象操作时都会先获取当前线程的ThreadLocalMap
,然后对ThreadLocalMap
进行操作,并且,ThreadLocal
不是简单的作为key的,而是将key和value包装成继承自弱引用WeakReference
的Entry
类。但这里要注意的是,弱引用只是针对key(Entry
中的ThreadLocal
),当没有任何强引用指向ThreadLocal
的时候,它就只剩下弱引用了,GC时将会被回收,但是value却不会被回收,因为它存在一条当前Thread->ThreadLocalMap->Entry数组->Entry->value
的强引用,所以除非线程销毁,否则它将与线程的生命周期绑定,尤其是在有线程复用比如线程池的场景中,一个线程的寿命很长,大对象长期不被回收会影响系统运行效率与安全,也就造成了人们常说的内存泄露。
但是在源码中我们也会发现,ThreadLocalMap
实现中是有一套自我清理的机制的,当我们调用get()
或者set()
方法时会有很高的概率顺便清理掉失效的Entry
,防止出现内存泄露。当然,显示地进行remove()
是个良好的编程习惯,它可以确保不会发生内存泄露。