前言
ThreadLocal用于多线程环境下每个线程存储和获取线程的局部变量,这些局部变量与线程绑定,线程之间互不影响。本篇文章将对ThreadLocal的运用和原理进行学习。
正文
一. ThreadLocal的运用
以一个简略比如对ThreadLocal的运用进行说明。
通常,ThreadLocal的运用是将其声明为类的私有静态字段,如下所示。
public class ThreadLocalLearn {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void setThreadName(String threadName) {
threadLocal.set(threadName);
}
public String getThreadName() {
return threadLocal.get();
}
}
ThreadLocalLearn类具有一个声明为private static的ThreadLocal目标字段,每个线程经过ThreadLocalLearn提供的setThreadName() 办法存放线程名,经过getThreadName() 办法获取线程名。
二. ThreadLocal的原理
首先剖析一下ThreadLocal的set() 办法,其源码如下所示。
public void set(T value) {
// 获取当时线程
Thread t = Thread.currentThread();
// 获取当时线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 以ThreadLocal目标为键,将value存到当时线程的ThreadLocalMap中
map.set(this, value);
else
// 假如当时线程没有ThreadLocalMap,则先创立,再存值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
由上面源码可知,ThreadLocal的set() 办法实践上是ThreadLocal以本身目标为键,将value存放到当时线程的ThreadLocalMap中。每个线程目标都有一个叫做threadLocals的字段,该字段是一个ThreadLocalMap类型的目标。ThreadLocalMap类是ThreadLocal类的一个静态内部类,用于线程目标存储线程独享的变量副本。
ThreadLocalMap实践上并不是一个Map,关于ThreadLocalMap是怎么存储线程独享的变量副本,将在后一末节进行剖析。下面再看一下ThreadLocal的get() 办法。
public T get() {
// 获取当时线程
Thread t = Thread.currentThread();
// 获取当时线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以ThreadLocal目标为键,从当时线程的ThreadLocalMap中获取value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T) e.value;
return result;
}
}
// 假如当时线程没有ThreadLocalMap,则创立ThreadLocalMap,并以ThreadLocal目标为键存入一个初始值到创立的ThreadLocalMap中
// 假如有ThreadLocalMap,但获取不到value,则以ThreadLocal目标为键存入一个初始值到ThreadLocalMap中
// 回来初始值,且初始值一般为null
return setInitialValue();
}
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;
}
由上面源码可知,ThreadLocal的get() 办法实践上是ThreadLocal以本身目标为键,从当时线程的ThreadLocalMap中获取value。
经过剖析ThreadLocal
的set()
和get()
办法可知,ThreadLocal
能够在多线程环境下存储和获取线程的局部变量,本质是将局部变量值存放在每个线程目标的ThreadLocalMap
中,因而线程之间互不影响。
三. ThreadLocalMap的原理
ThreadLocalMap本身不是Map,但是能够实现以key-value的方式存储线程的局部变量。与Map类似,ThreadLocalMap中将键值对的联系封装为了一个Entry目标,Entry是ThreadLocalMap的静态内部类,源码如下所示。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry承继于WeakReference,因而Entry是一个弱引证目标,而作为键的ThreadLocal目标是被弱引证的目标。
首先剖析ThreadLocalMap的结构函数。ThreadLocalMap有两个结构函数,这儿只剖析签名为ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
的结构函数。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创立一个容量为16的Entry数组
table = new Entry[INITIAL_CAPACITY];
// 运用散列算法核算第一个键值对在数组中的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创立Entry目标,并存放在Entry数组的索引对应方位
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 依据Entry数组初始容量巨细设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap的散列算法为将ThreadLocal的哈希码与Entry数组长度减一做相与操作,因为Entry数组长度为2的幂次方,因而上述散列算法本质是ThreadLocal的哈希码对Entry数组长度取模。经过散列算法核算得到初始键值对在Entry数组中的方位后,会创立一个Entry目标并存放在数组的对应方位。最后依据公式:len * 2 / 3核算扩容阈值。
由上述剖析可知,创立ThreadLocalMap目标时便会初始化存放键值对联系的Entry数组。现在看一下ThreadLocalMap的set() 办法。
// 调用set()办法时会传入一对键值对
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 经过散列算法核算键值对的索引方位
int i = key.threadLocalHashCode & (len-1);
// 遍历Entry数组
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取当时Entry的键
ThreadLocal<?> k = e.get();
// 当时Entry的键与键值对的键持平(即指向同一个ThreadLocal目标),则更新当时Entry的value为键值对的值
if (k == key) {
e.value = value;
return;
}
// 当时Entry的键被废物收回了,这样的Entry称为陈腐项,则依据键值对创立Entry并替换陈腐项
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 此刻i标明遍历Entry数组时遇到的第一个空槽的索引
// 程序运行到这儿,说明遍历Entry数组时,在遇到第一个空槽前,遍历过的Entry的键与键值对的键均不持平,一起也没有陈腐项
// 此刻依据键值对创立Entry目标并存放在索引为i的方位(即空槽的方位)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// Entry数组中键值对数量大于等于阈值,则触发rehash()
// rehash()会先遍历Entry数组并删去陈腐项,假如删去陈腐项之后,键值对数量还大于等于阈值的3/4,则进行扩容
// 扩容后,Entry数组长度应该为扩容前的两倍
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
在set() 办法中,首先经过散列算法核算键值对的索引方位,然后从核算得到的索引方位开端往后遍历Entry数组,一向遍历到第一个空槽停止。在遍历的过程中,假如遍历到某个Entry的键与键值对的键持平,则更新这个Entry的值为键值对的值;假如遍历到某个Entry而且这个Entry被判定为陈腐项(键被废物收回的Entry目标),那么履行铲除陈腐项的逻辑;假如遍历遇到空槽了,但没有发现有键与键值对的键持平的Entry,也没有陈腐项,则依据键值对生成Entry目标并存放在空槽的方位。
在set() 办法中,需求铲除陈腐项时调用了replaceStaleEntry() 办法,该办法会依据键值对创立Entry目标并替换陈腐项,一起触发一次铲除陈腐项的逻辑。replaceStaleEntry() 办法的实现如下所示。
// table[staleSlot]为陈腐项
// 该办法实践便是从索引staleSlot开端向后遍历Entry数组直到遇到空槽,假如找到某一个Entry的键与键值对的键持平,那么将这个Entry的值更新为键值对的值,并将这个Entry与陈腐项交换方位
// 假如遇到空槽也没有找到键与键值对的键持平的Entry,则直接将陈腐项铲除,然后依据键值对创立一个Entry目标存放在索引为staleSlot的方位
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
// 从索引为staleSlot的槽位向前遍历Entry数组直到遇到空槽,并记录遍历时遇到的最后一个陈腐项的索引,用slotToExpunge标明
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从索引为staleSlot的槽位向后遍历Entry数组
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 假如遍历到某个Entry的键与键值对的键持平
if (k == key) {
// 将遍历到的Entry的值更新
e.value = value;
// 将更新后的Entry与索引为staleSlot的陈腐项交换方位
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 假如向前遍历Entry数组时没有发现陈腐项,那么这儿将slotToExpunge的值更新为陈腐项的新方位的索引
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// expungeStaleEntry(int i)能够铲除i方位的陈腐项,以及从i方位的槽位到下一个空槽之间的一切陈腐项
// cleanSomeSlots(int i, int n)能够从i方位开端向后扫描log2(n)个槽位,假如发现了陈腐项,则铲除陈腐项,并再向后扫描log2(table.length)个槽位
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 假如遍历到的Entry是陈腐项,而且向前遍历Entry数组时没有发现陈腐项,则将slotToExpunge的值更新为当时遍历到的陈腐项的索引
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 从索引为staleSlot的槽位向后遍历Entry数组时,直到遇到了空槽也没有找到键与键值对的键持平的Entry
// 此刻将staleSlot方位的陈腐项直接铲除,并依据键值对创立一个Entry目标存放在索引为staleSlot的方位
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 一开端时,staleSlot与slotToExpunge是持平的,一旦staleSlot与slotToExpunge不持平,标明从staleSlot方位向前或向后遍历Entry数组时,发现了除staleSlot方位的陈腐项之外的陈腐项
// 此刻需求铲除这些陈腐项
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
在replaceStaleEntry() 中调用了两个关键办法,expungeStaleEntry(int i) 能够铲除i方位的陈腐项,以及从i方位的槽位到下一个空槽之间的一切陈腐项;cleanSomeSlots(int i, int n) 能够从i方位开端向后扫描log2(n) 个槽位,假如发现了陈腐项,则铲除陈腐项,并再向后扫描log2(table.length) 个槽位。其实现如下。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 删去staleSlot方位的陈腐项
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 从staleSlot方位开端往后遍历Entry数组,直到遍历到空槽
// 假如遍历到陈腐项,则铲除陈腐项
// 假如遍历到非陈腐项,则将该Entry从头经过散列算法核算索引方位
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;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 回来遍历到的空槽的索引
return i;
}
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为Entry数组长度,然后铲除扫描到的陈腐项到下一个空槽之间的一切陈腐项,最后从空槽的方位向后再扫描log2(table.length)个槽位
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);
return removed;
}
因为replaceStaleEntry() 办法中对应了很多种情况,因而单纯依据代码不能很直观的了解ThreadLocalMap是怎么铲除陈腐项的,所以下面结合图进行学习。这儿默许Entry数组长度为16。
场景一:Entry
数组槽位散布如下所示。
从staleSlot向前遍历时,会将slotToExpunge值置为2,从staleSlot向后遍历时,因为索引为6的Entry目标的键与键值对的键持平,因而会更新这个Entry目标的值,并与staleSlot方位(索引为4)的陈腐项交换方位。交换方位后,Entry数组槽位散布如下所示。
因而最后会触发一次铲除陈腐项的逻辑。先铲除slotToExpunge到下一个空槽之间的一切陈腐项,即索引2和索引6的槽位的陈腐项会被铲除;然后从空槽的下一个槽位,往后扫描log2(16) = 4个槽位,即顺次扫描索引为8,9,10,11的槽位,并在扫描到索引为10的槽位时发现陈腐项,此刻铲除索引10槽位到下一个空槽之间的一切陈腐项,即索引10槽位的陈腐项会被铲除,再然后从空槽的下一个槽位往后扫描log2(16) = 4个槽位,即顺次扫描索引为13,14,15,0的槽位,没有发现陈腐项,扫描完毕,并回来true,标明扫描到了陈腐项并铲除了。
场景二:Entry
数组槽位散布如下所示。
从staleSlot向前遍历时,直到遇到空槽停止,也没有陈腐项,因而向前遍历完毕后,slotToExpunge与staleSlot持平。向后遍历到索引5的槽位时,发现了陈腐项,因为此刻slotToExpunge与staleSlot持平,因而将slotToExpunge置为5。继续向后遍历,因为索引为6的Entry目标的键与键值对的键持平,因而会更新这个Entry目标的值,并与staleSlot方位(索引为4)的陈腐项交换方位。交换方位后,Entry数组槽位散布如下所示。
因而最后会触发一次铲除陈腐项的逻辑,铲除逻辑与场景一相同,这儿不再赘述。
场景三:Entry
数组槽位散布如下所示。
从staleSlot向前遍历时,会将slotToExpunge值置为2,从staleSlot向后遍历时,直到遇到空槽停止,也没有发现键与键值对的键持平的Entry,因而会将索引为staleSlot的槽位的陈腐项直接铲除,并依据键值对创立一个Entry目标存放在索引为staleSlot的方位。staleSlot槽位的陈腐项被铲除后的槽位散布如下所示。
之后铲除陈腐项的逻辑与场景一相同,这儿不再赘述。
实践场景下可能不会呈现上述的槽位散布,这儿仅仅举个比如,对replaceStaleEntry() 办法的履行流程进行说明。
下面再看一下getEntry() 办法。
private Entry getEntry(ThreadLocal<?> key) {
// 运用散列算法核算索引
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 假如Entry数组索引方位的Entry的键与key持平,则回来这个Entry
return e;
else
// 没有找到key对应的Entry时会履行getEntryAfterMiss()办法
return getEntryAfterMiss(key, i, e);
}
// 该办法一边遍历Entry数组寻觅键与key持平的Entry,一边铲除陈腐项
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)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
无论是set() 仍是getEntry() 办法,一旦发现了陈腐项,便会触发铲除Entry数组中的陈腐项的逻辑,这是ThreadLocal为了避免产生内存走漏的维护机制。
四. ThreadLocal怎么避免内存走漏
已知,每个线程有一个ThreadLocalMap字段,ThreadLocalMap中将键值对的联系封装为了一个Entry目标,Entry是ThreadLocalMap的静态内部类,其实现如下。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
当正常运用ThreadLocal时,虚拟机栈和堆上目标的引证联系能够用下图标明。
因而Entry是一个弱引证目标,key引证的ThreadLocal为被弱引证的目标,value引证的目标(上图中的Object)为被强引证的目标,那么在这种情况下,key引证的ThreadLocal不存在其它引证后,在下一次废物收回时key引证的ThreadLocal会被收回,避免了ThreadLocal目标的内存走漏。key引证的ThreadLocal被收回后,此刻这个Entry就成为了一个陈腐项,假如不对陈腐项做铲除,那么陈腐项的value引证的目标就永远不会被收回,也会产生内存走漏,所以ThreadLocal采用了线性探测来铲除陈腐项,然后避免了内存走漏。
总结
合理运用ThreadLocal能够在多线程环境下存储和获取线程的局部变量,而且将ThreadLocalMap中的Entry规划成了一个弱引证目标,能够避免ThreadLocal目标的内存走漏,一起也采用了线性探测办法来铲除陈腐项,避免了Entry中的值的内存走漏,不过仍是建议在每次运用完ThreadLocal后,及时调用ThreadLocal的remove() 办法,及时开释内存。