ThreadLocal为当前线程创建和维护变量副本,并对其他线程不可见,确保线程安全,本文通过探究源码来深入了解其原理.
先看set()方法的实现:
public void set(T value) {
Thread t = Thread.currentThread();
//从当前线程Thread中获取ThreadLocalMap实例。
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocal实例和value封装成Entry。
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
看看map.set()方法如何将Entry存入table数组:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过ThreadLocal的nextHashCode方法生成hash值
//通过 hash & (len -1) 定位到table的位置i
//至于为什么HASH_INCREMENT = 0x61c88647,下篇文章再来分析
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果k和当前ThreadLocal实例一致,则替换value
if (k == key) {
e.value = value;
return;
}
//如果k为null,说明这个位置的元素已经是stale(陈旧的)的元素。调用replaceStaleEntry方法删除table中所有陈旧的元素(即entry的引用为
//null并插入新元素
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果Entry e = null,则把Entry加入到table的i位置中
//通过cleanSomeSlots删除陈旧的元素,如果table中没有元素删除,需判断当前情况下是否要进行扩容。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
table扩容:
如果table中的元素数量达到阈值threshold的3/4,会进行扩容操作,过程如下:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
//当阙值大小为哈希表长度的2/3 ,上面表明扩容时哈希表内元素数量为阙值的3/4,相乘为哈希表大小的1/2。即当哈希表内元素数量超过哈希表大小一半时,就会发生扩容。扩容后的大小为原大小的两倍
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private void resize() {
//旧数组
Entry[] oldTab = table;
//旧数组长度
int oldLen = oldTab.length;
//新数组长度 = 旧数组长度*2
int newLen = oldLen * 2;
//新数组
Entry[] newTab = new Entry[newLen];
//计数
int count = 0;
//复制table的元素到newTab,忽略陈旧的元素,假设table中的元素e需要复制到newTab的i位置,如果i位置存在元素,则找下一个空位置进行插入。
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;
}
看ThreadLocal.get() 实现:
public T get() {
Thread t = Thread.currentThread();
//获取当前的线程的threadLocals。
ThreadLocalMap map = getMap(t);
如果threadLocals不为null,则通过ThreadLocalMap.getEntry方法找到对应的entry
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
//如果其引用和当前key一致,则直接返回,否则在table剩下的元素中继续匹配。
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果threadLocals为null,则通过setInitialValue方法初始化,并返回。
return setInitialValue();
}
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);
}
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;
}
ThreadLocal的内存泄漏问题
之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题.
-
Entry对象是ThreadLocal的弱引用。所以当线程消亡时,垃圾回收器会将它回收,此时不存在内存泄漏问题。
-
但如果使用线程池,并且线程存在复用的情形,线程不会消亡,这些Entry对象就会发生泄漏,此时就需要我们进行手动清理。
-
清理的方法很简单,就是调用ThreadLocal的remove()方法,和上面的分析类似,它会不停探测索引,如果找到正确的ThreadLocal对象就将其清除。