ThreadLocal( 线程局部变量 )
在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用 ThreadLocal 辅助类为
各个线程提供各自的实例。
例如有一个静态变量
public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
如果两个线程同时调用 sdf.format(…)
那么可能会很混乱,因为 sdf 使用的内部数据结构可能会被并发的访问所破坏。当然可以使
用线程同步,但是开销很大;或者也可以在需要时构造一个局部 SImpleDateFormat 对象。
但这很浪费。
希望为每一个线程构造一个对象,即使该线程调用多次方法,也只需要构造一次,不必在局
部每次都构造。
public static final ThreadLocal
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 它,防止
内存泄露。