ThreadLocal再总结
前言
之前我的一篇文章Android消息机制——补充完善中大致分析过ThreadLocal,所以这篇文章主要为了补充完善关于它的相关知识。
思考
问:为什么每个线程中要持有一个ThreadLocal.ThreadLocalMap对象而不是ThreadLocal对象?
这个问题的答案,我觉得这篇文章讲的不错:threadlocal为什么这么设计?
文章分析了两种ThreadLocal的设计方案:
-
方案一:每个线程可以持有多个ThreadLocal对象,这些ThreadLocal对象将Thread的id(或者线程的名称等线程标识字段)作为key,想要存储的具体实例的引用作为value存储在其中的ThreadLocalMap中,如下图所示:
这种方式主要的问题是需要进行同步,效率较低。
因为ThreadLocal可能会被多个线程引用,所以在多线程环境下对其中的ThreadLocalMap进行操作就需要进行同步了。
-
方案二:每个线程持有ThreadLocal内部的ThreadLocalMap对象,这个ThreadLocalMap将外部类ThreadLocal对象的引用作为key,具体实例的引用作为value存储在其中,如下图所示。ThreadLocal就是使用的这种设计方案。
在这种方式下,每个线程直接持有一个ThreadLocalMap对象,各自线程的变量被各自线程中ThreadLocalMap对象的value所引用,每个线程对这个ThreadLocalMap对象的set、get等操作都不会影响其他线程,所以直接避免了线程同步的问题,效率较第一种方案高。
ThreadLocalMap的哈希冲突
threadlocalmap的set方法处理哈希冲突的方法为开放定址法中的线性探测再散列法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
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); //下标通过threadlocal的hashcode&(entry数组长度-1)获得
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
/*
这里的nextIndex方法就是为了处理哈希冲突
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
可以看到,它处理哈希冲突的办法为开放定址法中的线性探测再散列法,冲突时每次寻找后一个元素
*/
ThreadLocal<?> k = e.get();
if (k == key) { //如果当前位置有值,就将value覆盖
e.value = value;
return;
}
if (k == null) { //当前索引的entry还未使用,直接将key-value放入
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
一个小问题
ThreadLocal中使用的不是Entry数组存储吗,为什么能存储键值对呢?
我们来看看Entry数组的定义:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); //调用了父类WeakReference的构造方法
value = v;
}
}
public WeakReference(T referent) {
super(referent); //调用了父类Reference的构造方法
}
Reference(T referent) {
this(referent, null); //调用了下面的构造方法
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
可以看到,Entry间接继承于Reference,也就是说它从Reference类继承了referent这个成员变量,Entry的构造方法最终调用了Reference的构造方法,将ThreadLocal存储在了成员变量referent中作为 “键” 。
内存泄漏问题
原因:
- ThreadLocalMap的生命周期跟Thread一样长
- 发生GC时,弱引用Key指向的ThreadLocal对象会被回收,而Value强引用指向的对象可能不会被回收。
解决办法
在使用完ThreadLocal对象后,调用它的remove
方法来断开value强引用。
下面看看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;
}
}
}
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; //移除value强引用
tab[i] = null; //移除当前索引指向的的Entry对象
size--;
} 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;
}