ThreadLocal源码

ThreadLocal( 线程局部变量 )

在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用 ThreadLocal 辅助类为

各个线程提供各自的实例。

例如有一个静态变量

public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);

如果两个线程同时调用 sdf.format(…)

那么可能会很混乱,因为 sdf 使用的内部数据结构可能会被并发的访问所破坏。当然可以使

用线程同步,但是开销很大;或者也可以在需要时构造一个局部 SImpleDateFormat 对象。

但这很浪费。

希望为每一个线程构造一个对象,即使该线程调用多次方法,也只需要构造一次,不必在局

部每次都构造。

public static final ThreadLocal sdf = new

ThreadLocal() {

@Override

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("yyyy-MM-dd");

}

};

实现原理:

每个线程的变量副本是存储在哪里的?

ThreadLocal 的 get 方法就是从当前线程的 ThreadLocalMap 中取出当前线程对应的变量的

副本。该 Map 的 key 是 ThreadLocal 对象,value 是当前线程对应的变量。

public T get() {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

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) {

return t.threadLocals;

}

【注意,变量是保存在线程中的,而不是保存在 ThreadLocal 变量中】。当前线程中,有一

个变量引用名字是 threadLocals,这个引用是在 ThreadLocal 类中 createmap 函数内初始化

的。

void createMap(Thread t, T firstValue) {

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

每个线程都有一个这样的名为 threadLocals 的 ThreadLocalMap,以 ThreadLocal 和

ThreadLocal 对象声明的变量类型作为 key 和 value。

Thread

ThreadLocal.ThreadLocalMap threadLocals = null;

这样,我们所使用的 ThreadLocal 变量的实际数据,通过 get 方法取值的时候,就是通过取

出 Thread 中 threadLocals 引用的 map,然后从这个 map 中根据当前 threadLocal 作为参数,

取出数据。

每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含

若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;

Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:

为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;

Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。

为什么 ThreadLocalMap 的 Key 是弱引用?

如果是强引用,ThreadLocal 将无法被释放内存。

因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期

与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回

收,而程序本身也无法判断是否可以清理节点。弱引用是 Java 中四档引用的第三档,比软

引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个

ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry

的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。

ThreadLocalMap 是何时初始化的(setInitialValue)?

在 get 时最后一行调用了 setInitialValue,它又调用了我们自己重写的 initialValue 方法获得

private T setInitialValue() {

T value = initialValue();

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null)

map.set(this, value);

else

createMap(t, value);

return value;

}

ThreadLocalMap 原理

static class Entry extends WeakReference> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal k, Object v) {

super(k);

value = v;

}

}

它也是一个类似 HashMap 的数据结构,但是并没实现 Map 接口。

也是初始化一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不

过这里的 key 永远都是 ThreadLocal 对象,通过 ThreadLocal 对象的 set 方法,结果把

ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中。

ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap 很大的区别是,Entry 中没有

next 字段,所以就不存在链表的情况了

构造方法

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {

// 表的大小始终为 2 的幂次

table = new Entry[INITIAL_CAPACITY];

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

table[i] = new Entry(firstKey, firstValue);

size = 1;

// 设定扩容阈值

setThreshold(INITIAL_CAPACITY);

}

在 ThreadLocalMap 中,形如 key.threadLocalHashCode & (table.length - 1)(其中 key 为一

个 ThreadLocal 实例)这样的代码片段实质上就是在求一个 ThreadLocal 实例的哈希值,只

是在源码实现中没有将其抽为一个公用函数。

对于& (INITIAL_CAPACITY - 1),相对于 2 的幂作为模数取模,可以用&(2^n-1)来替代%2^n,

位运算比取模效率高很多。至于为什么,因为对 2^n 取模,只要不是低 n 位对结果的贡献

显然都是 0,会影响结果的只能是低 n 位。

private void setThreshold(int len) {

threshold = len * 2 / 3;

}

getEntry(由 ThreadLocal#get 调用)

private Entry getEntry(ThreadLocal key) {

int i = key.threadLocalHashCode & (table.length - 1);

Entry e = table[i];

if (e != null && e.get() == key)

return e;

else

// 因为用的是线性探测,所以往后找还是有可能能够找到目标 Entry 的。

return getEntryAfterMiss(key, i, e);

}

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {

Entry[] tab = table;

int len = tab.length;

while (e != null) {

ThreadLocal k = e.get();

if (k == key)

return e;

if (k == null)

// 该 该 entry 对应的 ThreadLocal 已经被回收,调用 expungeStaleEntry 来清

理无效的 entry

expungeStaleEntry(i);

else

i = nextIndex(i, len);

e = tab[i];

}

return null;

}

i 是位置

从 staleSlot 开始遍历,将无效 key(弱引用指向对象被回收)清理,即对应 entry 中的 value

置为 null,将指向这个 entry 的 table[i]置为 null,直到扫到空 entry。

另外,在过程中还会对非空的 entry 作 rehash。

可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot(断开强引用,rehash slot

等)

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 {

// 对于还没有被回收的情况,需要做一次 rehash。

如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置 i,

则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。

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;

}

set(线性探测法解决 hash 冲突)

private void set(ThreadLocal key, Object value) {

// We don't use a fast path as with get() because it is at

// least as common to use set() to create new entries as

// it is to replace existing ones, in which case, a fast

// path would fail more often than not.

Entry[] tab = table;

int len = tab.length;

// 计算 key 的 的 hash 值

int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];

e != null;

e = tab[i = nextIndex(i, len)]) {

ThreadLocal k = e.get();

if (k == key) {

// 同一个 ThreadLocal 赋了新值,则替换原值为新值

e.value = value;

return;

}

if (k == null) {

// 该位置的 TheadLocal 已经被回收,那么会清理 slot 并在此位置放入当前 key

和 value(stale:陈旧的)

replaceStaleEntry(key, value, i);

return;

}

}

// 下一个位置为空,那么就放到该位置上

tab[i] = new Entry(key, value);

int sz = ++size;

// 启发式地清理一些 slot,并判断是否是否需要扩容

if (!cleanSomeSlots(i, sz) && sz >= threshold)

rehash();

}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal

对象,hash 值就增加一个固定的大小 0x61c88647。

private final int threadLocalHashCode = nextHashCode();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {

return nextHashCode.getAndAdd(HASH_INCREMENT);

}

由于 ThreadLocalMap 使用线性探测法来解决散列冲突,所以实际上 Entry[]数组在程序逻辑

上是作为一个环形存在的。

private static int nextIndex(int i, int len) {

return ((i + 1 < len) ? i + 1 : 0);

}

在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i,过程如下:

1、如果当前位置是空的,那么正好,就初始化一个 Entry 对象放在位置 i 上;

2、不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,

那么重新设置 Entry 中的 value;

3、很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;

这样的话,在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,

然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位

可以发现,set 和 get 如果冲突严重的话,效率很低,因为 ThreadLoalMap 是 Thread 的一

个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

cleanSomeSlots(启发式地清理 slot)

i 是当前位置,n 是元素个数

i 对应 entry 是非无效(指向的 ThreadLocal 没被回收,或者 entry 本身为空)

n 是用于控制控制扫描次数的

正常情况下如果 log n 次扫描没有发现无效 slot,函数就结束了

但是如果发现了无效的 slot,将 n 置为 table 的长度 len,做一次连续段的清理

再从下一个空的 slot 开始继续扫描

这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效 slot

的时候可能会被调用,

区别是前者传入的 n 为元素个数,后者为 table 的容量

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;

}

rehash

先全量清理,如果清理后现有元素个数超过负载,那么扩容

private void rehash() {

// 进行一次全量清理

expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis

if (size >= threshold - threshold / 4)

resize();

}

全量清理

private void expungeStaleEntries() {

Entry[] tab = table;

int len = tab.length;

for (int j = 0; j < len; j++) {

Entry e = tab[j];

if (e != null && e.get() == null)

expungeStaleEntry(j);

}

}

扩容,因为需要保证 table 的容量 len 为 2 的幂,所以扩容即扩大 2 倍

private void resize() {

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) {

ThreadLocal k = e.get();

if (k == null) {

e.value = null; // Help the GC

} else {

int h = k.threadLocalHashCode & (newLen - 1);

while (newTab[h] != null)

h = nextIndex(h, newLen);

newTab[h] = e;

count++;

}

}

}

setThreshold(newLen);

size = count;

table = newTab;

}

remove

private void remove(ThreadLocal key) {

Entry[] tab = table;

int len = tab.length;

int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];

e != null;

e = tab[i = nextIndex(i, len)]) {

if (e.get() == key) {

// 显式断开弱引用

e.clear();

// 进行段清理

expungeStaleEntry(i);

return;

}

}

}

Reference#clear

public void clear() {

this.referent = null;

}

内存泄露

只有调用 TheadLocal 的 remove 或者 get、set 时才会采取措施去清理被回收的 ThreadLocal

对应的 value(但也未必会清理所有的需要被回收的 value)。假如一个局部的 ThreadLocal

不再需要,如果没有去调用 remove 方法清除,那么有可能会发生内存泄露。

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get()、set()

可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots

可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry

对象。

如果使用 ThreadLocal 的 set 方法之后,没有显式的调用 remove 方法,就有可能发生内存

泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方

法。

JDK 建议将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就

更长,由于一直存在 ThreadLocal 的强引用,所以 ThreadLocal 也就不会被回收,也就能保

证任何时候都能根据 ThreadLocal 的弱引用访问到 Entry 的 value 值,然后 remove 它,防止

内存泄露。

你可能感兴趣的:(ThreadLocal源码)