ThreadLocal 存储线程本地变量,也就是说,每个线程都持有自己的一个副本,独立进行初始化和修改操作。
# 原理
# set & get
先来看下 set 和 get 操作。
public void set(T value) { | |
// 当前线程 | |
Thread t = Thread.currentThread(); | |
// 从 map 中获取当前线程存储的所有 ThreadLocal 变量 | |
ThreadLocalMap map = getMap(t); | |
//map 不为空的话直接设置当前 ThreadLocal 变量的值 | |
if (map != null) | |
map.set(this, value); | |
//map 为空的话创建 map 并设置当前 ThreadLocal 变量的值 | |
else | |
createMap(t, value); | |
} | |
public T get() { | |
// 获取当前线程 | |
Thread t = Thread.currentThread(); | |
// 获取当前线程的所有 ThreadLocal 变量 | |
ThreadLocalMap map = getMap(t); | |
//map 不为空且当前变量的值不为空,获取当前 ThreadLocal 变量的值,否则创建 map 并将当前线程的变量值设为 null,最终返回 null | |
if (map != null) { | |
ThreadLocalMap.Entry e = map.getEntry(this); | |
if (e != null) { | |
@SuppressWarnings("unchecked") | |
T result = (T)e.value; | |
return result; | |
} | |
} | |
return setInitialValue(); | |
} | |
ThreadLocalMap getMap(Thread t) { | |
// 当前线程所有的 ThreadLocal 变量存储在 Thread 中 | |
return t.threadLocals; | |
} |
可以看出,每个线程都有一个 ThreadLocalMap,且这个 map 是 Thread 中第一个变量。
# ThreadLocalMap
# Entry
存储使用 Entry 数组,Entry 是弱引用,是为了防止线程还存活,但是线程中的本地变量已不再被外界使用。
static class Entry extends WeakReference<ThreadLocal<?>> { | |
// 线程本地变量 | |
Object value; | |
// ThreadLocal 变量作为 key,是弱引用,变量值作为 value | |
Entry(ThreadLocal<?> k, Object v) { | |
super(k); | |
value = v; | |
} | |
} |
# 变量
默认初始容量为 16,动态进行扩容,当容量使用到达数组长度的 2/3 时,进行扩容。
// 数组默认初始化容量 | |
private static final int INITIAL_CAPACITY = 16; | |
// 存储所有本地变量 | |
private Entry[] table; | |
// 所有本地变量的个数 | |
private int size = 0; | |
// 扩容的阈值,一般设置为数组长度的 2/3 | |
private int threshold; |
# 构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { | |
// 初始化数组 | |
table = new Entry[INITIAL_CAPACITY]; | |
// 元素放入数组下标位置为本地变量哈希值与操作数组下标最大值 | |
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); | |
// 将元素放入对应位置 | |
table[i] = new Entry(firstKey, firstValue); | |
// 数组大小设置为 1 | |
size = 1; | |
// 设置扩容阈值为当前大小的 2/3 | |
setThreshold(INITIAL_CAPACITY); | |
} | |
private ThreadLocalMap(ThreadLocalMap parentMap) { | |
// 参数 map 的数组 | |
Entry[] parentTable = parentMap.table; | |
// 初始化大小为指定 map 的数组长度 | |
int len = parentTable.length; | |
setThreshold(len); | |
// 初始化当前数组 | |
table = new Entry[len]; | |
// 遍历参数 map 的数组,赋值给当前数组,注意这里不是按照下标一一对应的赋值,也是要计算本地变量的本地哈希值与操作数组下标最大值,如果新的下标有元素,放入后面的位置 | |
for (int j = 0; j < len; j++) { | |
Entry e = parentTable[j]; | |
if (e != null) { | |
@SuppressWarnings("unchecked") | |
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); | |
if (key != null) { | |
Object value = key.childValue(e.value); | |
Entry c = new Entry(key, value); | |
int h = key.threadLocalHashCode & (len - 1); | |
while (table[h] != null) | |
h = nextIndex(h, len); | |
table[h] = c; | |
size++; | |
} | |
} | |
} | |
} |
# 并发问题
可以看出,本地变量存储在每个线程上,所以每个线程只会访问自己的本地变量,不会访问到其他线程的,所以也不会有线程冲突问题。如果每个本地变量都是一个 map,那么 key 为线程标识,value 为变量值,这样的话会出现多个线程都要操作同一个 map 的场景。两者区别如图所示:
上图每个 map 需要被所有线程访问,下图每个 map 只需要被单个线程访问。
# 内存泄漏问题
内存泄漏常发生在线程复用的场景下,线程池中线程并不会被销毁,所以线程变量 threadLocals 也不会被销毁,但是 Entry 中的 key 是弱引用,当外部没有引用的情况下,ThreadLocal 会被销毁,也就是说,key 变成了 null。这种 key 为 null 的 Entry,在 set 或者 get 的时候,会进行清除,所以只要代码没有写搓,就不会发生内存泄漏。当然,最好在线程使用完毕前进行 clear。