前言
在并发编程中,被多个线程访问的共享变量可能会出现线程不安全的问题,而使用ThreadLocal存储的变量则不会出现线程安全问题,因为它是线程局部变量,只能被线程本身访问到。
ThreadLocal类上的javaDoc注释这样讲到:
This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code 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).
简单翻译一下:该类提供线程局部变量。这些变量与普通变量不同,因为每个访问一个线程(通过其get/set方法)的线程都有它自己的,独立初始化的变量副本。
ThreadLocal实例通常是作为将状态与线程关联的类中的私有静态字段(例如用户ID或交易ID)。
ThreadLocal 提供了一种方式,让在多线程环境下,每个线程都可以拥有自己独特的数据,并且可以在整个线程执行过程中,实现从上而下的传递。
ThreadLocal的作用是提供线程内的局部变量,在多线程环境下访问时能保证各个线程内的ThreadLocal变量各自独立。也就是说,每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。
ThreadLocal作为轻量级的存储存在,使得我们使用时不用考虑到它的线程安全问题。
ThreadLocal最常用于以下这个场景:多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程间共享,但是我们不想加锁,这时候可以使用ThreadLocal来使得每个线程都持有一个该对象的副本。
上面只是给出的一些直接的概念介绍,读者看到这里仍然会疑惑,
ThreadLocal到底是怎么实现只为当前线程服务的呢?不要着急,在底层原理模块,我们会慢慢展开。
使用场景
在这部分,我们会稍微看一下ThreadLocal的简单应用。
AOP中的一个类
在Spring AOP模块(对于AOP的原理,本文不会作详细说明),我们可以看到这样一个类:
public abstract class AopContext {
/**
* ThreadLocal holder for AOP proxy associated with this thread.
* Will contain {@code null} unless the "exposeProxy" property on
* the controlling proxy configuration has been set to "true".
* @see ProxyConfig#setExposeProxy
*/
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<Object>("Current AOP proxy");
public static Object currentProxy() throws IllegalStateException {
Object proxy = currentProxy.get();
if (proxy == null) {
throw new IllegalStateException(
"Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
}
return proxy;
}
static Object setCurrentProxy(Object proxy) {
Object old = currentProxy.get();
if (proxy != null) {
currentProxy.set(proxy);
}
else {
currentProxy.remove();
}
return old;
}
}
我们可以通过
setCurrentProxy和currentProxy方法分别对currentProxy对象进行写操作/读操作。大概可以猜到,
currentProxy实际上存储了一个代理对象,透过命名中的**"current",我们不难得知这个代理对象是 当前的,再一看到ThreadLocal,那就更清楚了,currentProxy指的是当前线程关联的代理对象**。
解决SimpleDateFormat的线程不安全问题
SimpleDateFormat是线程不安全的,我们可以通过ThreadLocal存储SimpleDateFormat对象以此消除线程不安全问题。
private static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
底层原理
ThreadLocal为何可以存储线程本地变量?
我们创建了一个ThreadLocal对象后,会使用set方法写入值。
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
我们发现ThreadLocal#set方法里会获取到当前的线程,然后再根据当前线程获取ThreadLocalMap对象。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这里就让人惊讶了,
ThreadLocalMap居然是Thread类中的一个变量。public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ... }
经过简单的分析后,发现ThreadLocal只是一个外壳,真正存储数据的是与Thread对象关联的ThreadLocaMap对象,因此我们说ThreadLocal可以用来存储线程本地变量,是因为这个ThreadLocalMap对象。
ThreadLocal(外壳)->Thread.ThreadLocalMap(容器)
数据结构
通过上面小节的讨论,我们发现针对
ThreadLocal底层数据结构的探究,其实就是对ThreadLocalMap内部结构的理解。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
上面是ThreadLocal#set方法,可以发现最终是调用了ThreadLocalMap#set方法
private void set(ThreadLocal<?> key, Object value) {
// 数组
Entry[] tab = table;
int len = tab.length;
// 使用hash值计算数组元素下标
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
...
}
...
}
-
ThreadLocalMap采用数组结构(Entry[])存储数据 -
Entry是一个弱引用实现,只要没有强引用存在,发生GC时就会被回收掉。static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } -
数据元素采用哈希散列方式进行存储,这里的散列使用的是
斐波那契(Fibonacci)散列法 -
由于这里不同于
HashMap的数据结构,发生哈希碰撞不会存成链表或红黑树,而是使用拉链法进行存储。也就是同一个下标位置发生冲突时,则
+1向后寻址,直到找到空位置或垃圾回收位置进行存储。
散列(hash)算法
既然
ThreadLocal是基于数组结构的拉链法存储,那么就一定会有哈希的计算。
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
神秘的0x61c88647
计算hash的方式,主要有除法散列法、平方散列法、斐波那契(Fibonacci)散列法、随机数法等方式。
而 ThreadLocal 使用的就是 斐波那契(Fibonacci)散列法 +拉链法存储数据到数组结构中。之所以使用斐波那契数列,是为了让数据更加散列,减少哈希碰撞。
0x61c88647魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
具体来自数学公式的计算求值,公式:f(k) = ((k * 2654435769) >> X) << Y
对于常见的32位整数而言,也就是
f(k) = (k * 2654435769) >> 28。
对于具体的数学公式,我们就不深究了,只要知道ThreadLocal采用了斐波那契数列使得数据更加分散,从而降低了哈希冲突的频率。
源码解读
ThreadLocalMap的构造
// 初始容量
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
/**
* table数组元素个数
*/
private int size = 0;
// 达到扩容条件的阈值
private int threshold; // Default to 0
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// 计算第一个存入元素的下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 先创建一个元素
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
// 16*2/3=10
threshold = len * 2 / 3;
}
set
ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 斐波那契散列,计算数组下标
int i = key.threadLocalHashCode & (len-1);
// 循环 判断 元素是否存在
// e表示 通过斐波那契散列算出来的数组下标 获取到的Entry元素
for (Entry e = tab[i];
// 数组元素不为空
e != null;
e = tab[i = nextIndex(i, len)]) { // 4. key 不相同,拉链法寻址
// 获取Entry元素的ThreadLocal对象(key)
ThreadLocal<?> k = e.get();
// 2.如果key相等,则替换掉原有的value
if (k == key) {
e.value = value;
return;
}
// 3. 如果key不相等 并且k为空 (弱引用发生GC时,产生的情况)
if (k == null) {
// 探测式清理过期元素
replaceStaleEntry(key, value, i);
return;
}
}
// 1.空位置 直接插入
tab[i] = new Entry(key, value);
// 数组中目前有的元素个数
int sz = ++size;
// cleanSomeSlots:启发式清理
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
}
扩容机制
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
cleanSomeSlots(i, sz)启发式清理,把过期元素清理掉- 如果没有清理掉任何过期元素,那么就会判断Entry数组元素个数是否超过了threshold,如果超过了就会扩容
扩容的核心方法为rehash。
private void rehash() {
// 探测式清理过期元素
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// size > = threshold * 3/4
if (size >= threshold - threshold / 4)
resize();
}
ThreadLocalMap#resize
private void resize() {
// 旧的Entry数组
Entry[] oldTab = table;
// 旧数组的长度
int oldLen = oldTab.length;
// 计算新数组的长度
int newLen = oldLen * 2;
// 创建新数组
Entry[] newTab = new Entry[newLen];
int count = 0;
// 循环遍历旧数组
for (int j = 0; j < oldLen; ++j) {
// 获取旧数组的元素
Entry e = oldTab[j];
// 判空
if (e != null) {
// 获取key
ThreadLocal<?> k = e.get();
// 如果key是空,则将value设置为空,帮助GC
if (k == null) {
e.value = null; // Help the GC
} else {
// 使用斐波那契数列重新计算元素的hash值
int h = k.threadLocalHashCode & (newLen - 1);
// 如果当前位置已有元素,证明发生了hash冲突,则使用拉链法找下面的位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 设置位置
newTab[h] = e;
// 计算新数组中元素个数
count++;
}
}
}
// 设置新的扩容阈值
setThreshold(newLen);
size = count;
// 将新数组关联给table变量
table = newTab;
}
启发式清理
ThreadLocal#cleanSomeSlots
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
while循环中不断的右移进行寻找需要被清理的过期元素,最终都会使用expungeStaleEntry进行处理,这里还包括元素的移位。
探测式清理
探测式清理,是以当前遇到的元素开始,向后不断的清理。直到遇到 null的Entry元素 为止,才停止 rehash 计算
Rehash until we encounter null。
ThreadLocalMap#expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
总结
本文简单介绍了ThreadLocal的原理,主要包含ThreadLocal如何实现线程间隔离、斐波那契散列降低哈希冲突、扩容、启发式清理和探测式清理等内容,由于本人水平有限,不少地方没有做更深入地探讨,实在惭愧!
本文并没有讲解内存泄漏问题,读者可以参考其他文章理解一下,强推大佬一枝花算不算浪漫的文章面试官:小伙子,听说你看过ThreadLocal源码
在对ThreadLocal的理解中,可以看到技术原理的底层离不开优秀的数据结构和算法,再想想Josh Bloch和Doug Lea写的代码,如果轻易就被我们看懂了,是不是也不大现实呢,继续加油吧,骚年!