今天看面经说,ThreadLocal对象调用get方法,会触发对value内存的释放。所以就阅读源码来验证一下。
前言
众所周知,ThreadLocal的实现原理,就是通过在每个线程对象中维护一个map,ThreadLocal每次操作时拿到调用者线程,得到对应map,以自己为key,操作当前线程的资源对象(value)。
其中的map里,维护Entity数组。Entity是一个继承了弱引用的内部类,使用父类构造器将Entity的key设置成了弱引用。
所以说,如果ThreadLocal对象没有其他强引用,那么在gc时,这个对象会被回收。
但是value不会被回收。
value什么时候被回收?这里有3个情况:
get(key)
时,发现对应位置的key是null。这时会释放掉value的内存(这里有一个和一般map不一样的地方,就是如果get(key)
发现对应位置的key是空的,就会往这个位置放一个key,然后值是null)set (key)
时,会使用启发式扫描,清除临近的null key的value,启发次数与元素个数,是否发现null key有关。如果元素多,就多扫描,发现了null key就多扫描。- 主动remove key时,会释放value内存。因为在使用ThreadLocal时,一般都作为静态变量,对ThreadLocal对象进行强引用,所以前两种方法一般不会管用,所以最好主动调用remove方法。
这次针对第一种情况展开探索。
正文
首先调用 ThreadLocal 对象的 get 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果释放value,肯定是在map中做的,因为map管理着k-v
ThreadLocalMap.Entry e = map.getEntry(this);//进入这个方法
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
map.getEntry(this) 源码
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//e.get() == key是在干啥?
//首先Entry继承了弱引用,而弱引用又继承了引用
//这个get方法是Reference<T>类中的方法
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
e.get() 源码
/**
* Returns this reference object's referent. If this reference object has
* been cleared, either by the program or by the garbage collector, then
* this method returns <code>null</code>.
*
* @return The object to which this reference refers, or
* <code>null</code> if this reference object has been cleared
*/
public T get() {
return this.referent;
}
注意注释中说的
If this reference object has been cleared, either by the program or by the garbage collector, then this method returns <code>null</code>.
如果这个对象被清理了,那么返回 null。否则返回这个对象
也就是说 e.get() == key
在e不为null的情况下有这么几种情况:
- 相等,说明找到了正确的Entity,返回e
- e.get()不为null,但是也不相等,说明出现了hash冲突
- e.get()为null,说明key被清理了
如果是后两种情况,就会进入else分支,那么看一下这个分支下,方法的源码
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
//如果 e 不是null,那么拿到这个 k
ThreadLocal<?> k = e.get();
if (k == key) //如果key相等直接返回e,因为这循环中,可能找到相等的k
return e;
if (k == null) //如果k是null,说明它被清理了
expungeStaleEntry(i);//翻译过来就是,消除不新鲜的实体,可以看看这个方法的实现
else //对应hash冲突的情况,那么开放寻址法继续查找
i = nextIndex(i, len);
e = tab[i];
}
//如果 e 本来就是 null ,那么说明当前线程中 map 里没有保存过对应的实体,
//这好说,`setInitialValue()` 把 k 放到map里,v是null。
return null;
}
expungeStaleEntry 源码
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//这里把value 和 entity 都置为null,这样gc的时候就将其清理了
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//因为清理了一个位置,所以需要进行重新计算hash,调整位置
//同时清理一路上的k为null的位置
// 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;
tab[i] = null;
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;
}
源码分析到这里就结束了。第一次自己分析集合以外的源码,记录一下