这篇文章只是写一下自己看ThreadLocal源码时的心得体会,对于具体的源码解析,不做太多的分析,这类文章网上已经有很多了。比如下面这几篇,写的都非常详细:
ThreadLocal中运用到弱引用的概念,在内部ThreadLocalMap中键是对ThreadLocal实例的弱引用,弱引用的特性是再每次gc时,弱没有其他强引用和软引用,该实例都会被当做垃圾进行回收。这样做是为了防止内存泄漏做的第一重保障,如果不使用弱引用,则该ThreadLocal实例则永远有引用存在,而永远不会被垃圾回收,不论上层应用是否已经将ThreadLocal的引用置空。
源码分析:
首先本人英语一般,由于直译困难,源码中几个重要的概念说明如下:
ThreadLocal的API较为简单,重点以get和set方法为入口,分析流程,只写个人心得,具体分析参见上面链接。
set:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//线程已有ThreadLocalMap,调用Map内部方法赋值(重点)
map.set(this, value);
else
//该线程没有绑定ThreadLocalMap,创建一个新的,复制并保存,较为简单
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);
// i为该键值对应该在的位置,但有可能发生Hash冲突
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
// 如果值相同,则更新,返回
if (k == key) {
e.value = value;
return;
}
// 值为空,则找到了一个stale entry, 内部方法处理entry替换,并扫描,返回
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//程序运行到此,说明没发生hash冲突,或者冲突了,但到下一个为null的entry,Map中都是
//正常entry,且没有相同key,进行正常赋值,此时i为新entry应该在的index
tab[i] = new Entry(key, value);
int sz = ++size;
//每次赋值操作都会触发一次清理扫描,但这里触发的扫描,是在没发现
//stale entry的情况下,故乐观处理,扫描范围小
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果扫描出stale entry,必然会删除,而每次只能增加一个entry,
//有删除发生则sz必然小于操作之前,所以有上面的短路与,这里也可以看出大师手笔,扣死每一个可以
//优化的地方。
rehash();
}
这里出现了两个重要方法:replaceStaleEntry(key, value, i)和cleanSomeSlots(i, sz) :
private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot):
//程序能够进行到此方法,则说明一定发现了stale entry,故在完成替换任务的前提下,要进行一次扫描
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//从当前位置扫描到前一个null entry,将最前面的stale entry序号记录在slotToExpunge
//slotToExpunge实际记录的是需要进行扫描的起始位置。
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
//注意,这个循环是从新增entry应该存在的位置开始的,如果这个if满足,说明需要更新entry,并修改entry的位置。
if (k == key) {
e.value = value;
//这个替换操作很巧妙,需要仔细体会引用和实体的概念及关系。赋值操作实际是将=后面的实体,赋给=前面的引用。
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//这个if如果成立,说明该stale entry之前(到前一个null)没有stale entry,而该stale entry又经过替换操作,编程一个正常的entry,故可以进一步后移扫描范围(环形队列,没有缩小范围),降低内存泄漏可能
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//执行扫描操作
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
//到这里程序就返回了,这里就有一个疑问在下面。
return;
}
//没找到k=key,不进行任何操作,只判断是否需要将起始位置后移。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
出了循环仍没找到k=key,则就将这个地方赋为正确值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果slotToExpunge == staleSlot,则说明从前一个null entry 到后一个null entry 只有这一个stale entry, 而这个stale entry也被赋了新值。故不需要扫描。
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
private boolean cleanSomeSlots(int i, int n)在上面两个方法中都出现了:
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;
}
}
replaceStaleEntry(key, value, i)方法循环中return引发的疑问:是否会出现这样的情况,导致替换操作不完善:
由于ThreadLocalMap是采用开放定址法(open addressing)解决hash冲突的(另参见分离链表法(separate chaining),hashMap),并且源码该方法的扫描仅仅是在目标位置前后两个null entry之间进行扫描,故有没有可能因为对stale entry的清除而在原有相同key的位置和目标位置之间出现了null entry,而导致扫描无法到达的情况,导致意外,如下图。
答案:我能想到的缺陷,Josh Bloch and Doug Lea两位大师一定能想到,解决方法就在stale entry的清除方法中:
private int expungeStaleEntry(int staleSlot):
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 置空,施放资源
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 对下个null entry之前的entry都进行rehash重新定位
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;
}
}
}
// 返回下个null entry的位置
return i;
}
可以看到,每次清除stale entry,都会对每一个entry进行一次rehash直到下一个null entry,这就完全避免了疑问里那种情况的发生,因为清除stale entry后,都会保证所有的entry,和他应该在的位置之间没有null entry存在(又一个大师手笔)。
以上,set的所有流程结束,接下来是get方法(set的几个重要方法清楚之后,get的理解就简单了):
入口:
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();
}
setInitialValue()方法较为简单,不多说,看map.getEntry(this)方法:
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);
}
}
其中重要的为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的所有重要方法分析完毕(重要在内部类ThreadLocalMap中),两个重要的get和set方法流程也走通一遍(还有其他API方法,较为简单,不多说,可以直接看源码)
可以看到,源码中对所有出现stale entry的地方都是非常敏感的,需要重点处理,因为这正是ThreadLocal发生内存泄漏的原因。
待验证疑问:若一个ThreadLocal被多个线程使用,在一个线程中,将ThreadLocal的引用置空,是否影响其他线程中该ThreadLocal的值,感觉是会影响的,因为所有线程的ThreadLocalMap虽然不同,但作为建的ThreadLocal实例都是同一个,如果ThreadLocal被置空,其他ThreadLocalMap中的ThreadLocal键则只有弱引用存在,会被回收。但根据ThreadLocal特性和实际应用,又不会出现上面的情况,所以这个疑问需要事后验证一下。