什么是ThreadLocal?它是属于线程自己的小仓库。关于它的使用与介绍看这一篇:ThreadLocal的介绍与使用
关于ThreadLocal的原理,理清四个角色关系:Thread,ThreadLocal,ThreadLocalMap,Entry:
在Thread中有个变量指向ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是ThreadLocal的静态内部类,当线程第一次执行set时,ThreadLocal会创建一个ThreadLocalMap对象,设置给Thread的threadLocals变量。
静态内部类常称为嵌套类,表明内部类对象不需要与其外围类对象之间的联系,也就是不持有外围类对象的this引用。
static class ThreadLocalMap {
Entry是ThreadLocalMap的静态内部类,继承自WeakReference,它对ThreadLocal的引用是弱引用,是实际存储ThreadLocal对象 到 线程局部变量 之间映射的容器。
static class Entry extends WeakReference<ThreadLocal<?>> {
一个ThreadLocalMap种储存多个Entry
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
一段程序可创建多个ThreadLocal对象,但线程Thread对象只持有一个ThreadLocalMap对象,不同ThreadLocal映射到 table 数组不同位置。
各角色关系图(虚线代表弱引用)
关于内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
虽然Entry
对ThreadLocal
的引用是弱引用,但是线程的栈中持有对ThreadLocal
的强引用,而当对象只存在弱引用时才会被回收,这就存在OOM的风险,有两种解决办法:1,主动将程序中对ThreadLocal
对象的引用置为null,这样该ThreadLocal
对象以及所有它的Entry
都将被清理,当然你可以创建多个ThreadLocal
对象,但是这并不是个好的解决办法,比如你还想之后继续使用该ThreadLocal
对象。2,主动调用ThreadLocal#remove
方法,清除当前线程的所有键值对,该方法会主动将Entry
对ThreadLocal
的弱引用置为null,key为null的Entry
会被清理。
关于Hash索引
采用hash来定位线程存储的的value。
ThreadLocalMap初始化时会创建大小为 16 的Entry[] table 数组
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);
}
关于位置的确定:key.threadLocalHashCode & (table.length - 1);
ThreadLocal 的 threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
使用一个 static
的原子属性 AtomicInteger nextHashCode
,确保不同ThreadLocal对象拥有相同的AtomicInteger对象,目的是将不同ThreadLocal的映射数据均匀分散开,分散间隔由 HASH_INCREMENT = 0x61c88647
控制 ,如大小为 16 的数组,第一个ThreadLocal对象在 0 位,第二个在 7 位,第三个在 14 位置,第四个为5…每次间隔为 7。
位置的计算 threadLocalHashCode & (INITIAL_CAPACITY - 1)
如何处理 hash 冲突?
采用线性探测法来处理冲突,从当前位置往后找寻空位,空位指的是table[ i ] = null 或是 table[ i ] .key = null,将Entry插入该位置。也就是说一个Entry要么在它的hash位置上,要么就在该位置往后的某一位置上。
源码分析
get 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 返回当前线程持有的ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
分为两步:
1,获取当前线程的ThreadLocalMap
对象,不为空则调用它的getEntry方法来获取存储值。
2,若当前线程之前未调用 set 存储值,则调用 setInitialValue 方法:ThreadLocal
的子类可以重写 initialValue 方法,并设置返回值,也就是你可以设置初始化值,这样在未 set 前 get 会将该初始值存储进 table 数组中,并返回该初始值。
先来看看第一步getEntry:
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
return getEntryAfterMiss(key, i, e);
}
1)计算该ThreadLocal
对象在table中的存储位置,获取该位置的Entry
2) 不存在:分两种1,该位置为空,返回null;2,该位置Entry
的key并非是当前的ThreadLocal
对象,这是由于线性探测法的影响,调用 getEntryAfterMiss方法:该方法从 i 位置开始往后找寻 key 指向当前 ThreadLocal
对象的Entry
,找到就返回,或遍历到空位时结束循环返回null;在找寻途中若发现 key 为 null 的Entry
,调用 expungeStaleEntry 方法对该段 run 进行修正,由于线性探测发 table 数组中的情况一定是一段一段连续的片段,我们将一个连续的片段称为 run。
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);// 修正table数组
else
i = nextIndex(i, len); // 即后一位
e = tab[i];
}
return null;
}
expungeStaleEntry : 作用是修正该段 run 片段,这里修正指的是:由于线性探测法的影响,Entry 可能并不在自己的 hash 位置上,比如一个Entry的hash 位置是 i 但实际它在后面的 j 位置,当我们在 i 到 j 之间发现某Entry 的key 为null,删除该 Entry,对后面的元素进行修正,也就是重新计算它们的hash 位置,目的是维持我们的线性探测数组 table 不被破坏,否则之后很可能会找不到Entry。这里并非是对后面所有的元素进行修正,它的范围是直到空位为止,这也很好理解。
可以看到重要的是你对线性探测法有一定的了解,推荐《算法》一书,里面有对hash冲突的解决办法分析:线性探测法 和 拉链法。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 置空该位置
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 将该位置之后直到空位的元素重新rehash,修正位置
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 若发现废弃的Entry,置空该位置,继续rehash后面的元素
if (k == null) {
e.value = null;
tab[i] = null;
size--;
// 对该位置的Entry进行rehash,重新确认新的位置
} 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;
}
再来看第二步的 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;
}
将 initialValue 方法的返回值存储,若 ThreadLocalMap 对象为 null,则调用 createMap 创建。这里 initialValue 方法默认返回null,ThreadLocal 的子类可重写以设置初始值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLocalMap 的构造函数:创建大小为16的Entry数组,构造Entry放入数组,
// 更新阀值
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);
}
// ThreadLocalMap#threshold
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
set方法
public void set(T value) {
Thread t = Thread.currentThread();
// 获取线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 调用ThreadLocalMap#set 构造Entry并插入table中
if (map != null)
map.set(this, value);
// 为null则创建ThreadLocalMap 对象赋给线程
// 并在初始化过程中构造Entry插入table中
else
createMap(t, value);
}
跳到 ThreadLocalMap#set
方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 计算hash位置
// hash冲突,往后找寻空位插入
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 已存在则跟新value
if (k == key) {
e.value = value;
return;
}
// 发现废弃的Entry
if (k == null) {
// 将key的entry插入到i位置,具体看下面分析
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 构造Entry
int sz = ++size;
// cleanSomeSlots:从 i 往后探测table数组的情况,具体分析在下面
// 该方法返回true,说面至少删除了一个废弃的Entry,也就是size小于等于之前
// 返回false又发现size达到阀值,调用rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
1,发现重复,更新value
2,未发现重复前先发现了废弃的Entry,调用replaceStaleEntry,Entry会被插入到该位置。
3,不是上面两种情况,构造Entry插入到空位 i 位置上,判断是否需要扩容。
replaceStaleEntry: 将 key 的 Entry 插入到 staleSlot 位置,这里分两种情况:1,在 staleSlot 后发现相同的key;2,没有发现,构造新的 Entry。在过程中还会根据情况修正run,以及探测table数组情况并根据情况修正。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge 表示该run的第一个废弃Entry的位置,
// 之所以要找第一个,目的就是为了之后调用 expungeStaleEntry
// 修正该run片段之用
int slotToExpunge = staleSlot;
// 往前找
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从staleSlot 往后遍历该run
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//找到相同的key
if (k == key) {
e.value = value;//更新value
// 将staleSlot位置的Entry与i位置对换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 若想等则说明该run在i之前并无废弃的Entry,
// 由于上面的互换,该run的第一个废弃Entry位置变为了 i
// 所以将slotToExpunge置为i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// expungeStaleEntry:从slotToExpunge开始修正该run,
// 返回该run的结尾位置,结尾指的是为空的位置
// cleanSomeSlots:探测下一个run的情况,发现废弃就进行修正
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 发现废弃Entry,若它是该run段的第一个废弃Entry,
// 则更改slotToExpunge
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 若是未发现 key,则构造新Entry插入 staleSlot 位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 该run中还有废弃的Entry
if (slotToExpunge != staleSlot)
// expungeStaleEntry 从slotToExpunge往后修正该run,
// 返回值代表该段run的结尾后的那个空位置下标
// cleanSomeSlots 从该位置往后检查下一个run片段的情况
// 并对其进行修正
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
cleanSomeSlots:正如方法的名字,该方法就是清理一些废弃的Entry,那么一些是多少? 每次循环都将 n 减半直到为0为止,该该过程中若未发现废弃Entry,则中止返回 false;若发现了则将 n 重置为 table.length,也就是发现一个就延长探测范围,去发现更多,最终返回true。
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];
// 发现废弃元素,重置n,延长探测范围
if (e != null && e.get() == null) {
n = len;
removed = true; // 表明删除过废弃的Entry
// expungeStaleEntry方法上面分析过,就是修正该run片段
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
rehash:首先清理table中所有的run片段,之后若仍需扩容的话,调用resize。
private void rehash() {
expungeStaleEntries();//对整个table数组中所有run进行修正
// Use lower threshold for doubling to avoid hysteresis
// 采用较低的阀值,影响线性探测法性能的便是run的长度,低阀值保证了
// table 中不会出现过长的run。
if (size >= threshold - threshold / 4)
resize(); // 扩容
}
// 对整个table数组中所有run进行修正
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);
}
}
resize:扩容操作,table 扩大为原来的两倍,扩容中不会将废弃的Entry移到新数组中。
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删除操作
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap#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) {
// 将Entry对ThreadLocalMap对象的引用置空,以便被GC回收
e.clear();
expungeStaleEntry(i);// 删除i位置并修正该run
return;
}
}
}
关于线程安全性
每个线程都有自己的ThreadLocalMap,以及Entry[] 数组,只有自己操作,所以是线程安全的。那么ThreadLocal呢?它并没有可更改的状态,所以也是线程安全的,来看看它的三个成员变量
// 每个ThreadLocal对象初始化后都会得到自己的hash值,之后不会再变
private final int threadLocalHashCode = nextHashCode();
// 静态对象AtomicInteger,与ThreadLocal对象无关,
// 在第一次ThreadLocal类加载时初始化
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 不可变
private static final int HASH_INCREMENT = 0x61c88647;
所以说 ThreadLocal 也是线程安全的。
关于OOM的例子分析
推荐两篇博客
ThreadLocal造成OOM内存溢出案例演示与原理分析
借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference
关于引用队列
下面是引用 借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference这篇博客,因为之前没用过引用队列,所以在这里写下来,熟悉一下。
在很多场景中,我们的程序需要在一个对象的可达性(GC可达性,判断对象是否需要回收)发生变化的时候得到通知,引用队列就是用于收集这些信息的队列。
在创建SoftReference对象时,可以为其关联一个引用队列,当SoftReference所引用的对象被回收的时候,Java虚拟机就会将该SoftReference对象添加到预支关联的引用队列中。
需要检查这些通知信息时,就可以从引用队列中获取这些SoftReference对象。
不仅仅是SoftReference支持使用引用队列,软引用和虚引用也可以关相应的引用队列。
例子:
public class WeakCache {
private void printReferenceQueue(ReferenceQueue<Object> referenceQueue) {
WeakEntry sv;
while ((sv = (WeakEntry) referenceQueue.poll()) != null) {
System.out.println("引用队列中元素的key:" + sv.key);
}
}
private static class WeakEntry extends WeakReference<Object> {
private Object key;
WeakEntry(Object key, Object value, ReferenceQueue<Object> referenceQueue) {
//调用父类的构造函数,并传入需要进行关联的引用队列
super(value, referenceQueue);
this.key = key;
}
}
public static void main(String[] args) {
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
User user = new User("xuliugen", "123456");
WeakCache.WeakEntry weakEntry = new WeakCache.WeakEntry("654321", user, referenceQueue);
System.out.println("还没被回收之前的数据:" + weakEntry.get());
user = null;
System.gc(); //强制执行GC
System.runFinalization();
System.out.println("已经被回收之后的数据:" + weakEntry.get());
new WeakCache().printReferenceQueue(referenceQueue);
}
}
ReferenceQueue引用队列记录了GC收集器回收的引用,这样的话,我们就可以通过引用队列的数据来判断引用是否被回收,以及被回收之后做相应的处理,例如:如果使用弱引用做缓存则需要清除缓存,或者重新设置缓存等。