如果看过ThreadLocal的源码,你就会恍然发现weakHashMap的源码看着居然有点眼熟。ThreadLocal,WeakHashMap看似八竿子打不着的两个类居然会有很多相似的地方。
threadLocal实现原理:
每一个thread内部都有一个数据结构
ThreadLocal.ThreadLocalMap threadLocals = null;
当多线程在各自线程内部调用threadlocal.set(xxx)时,实际上就是找到各自线程的threadLocals,然后把xxx保存到这个threadlocals上面,由于该threadlocals是每个线程单独一份,所以只要保存的xxx也是线程单独一份那么就能实现线程间的隔离了。如果xxx是一个对象的引用,多个线程的xxx都同时指向一份堆内存,那么还是存在线程同步访问的问题,需要特别注意。
现在问题就是这个threadLocals是一个什么样的数据结构。
threadLocals实际上就是一个数组,每个数组成员是一个entry对象,结构如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
this.value = v;
}
}
可以看到一个entry上保存了key和value,其中key是一个弱引用类型,保存的是一个threadLocal对象。当线程中的没有强引用指向threadLocal时,在进行gc时entry中的这个key随时都会被回收,此时就会存在一个问题,entry上的key已经被收回,但是value还一直保存着,会不会引起内存泄露?
threadLocal已经考虑到了这个问题,所以内部有一个方法会对数组上entry的key被回收的value进行进行清除操作
private int expungeStaleEntry(int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = this.table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
--this.size;
ThreadLocal.ThreadLocalMap.Entry e;
int i;
for(i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = (ThreadLocal)e.get();
if (k == null) {
e.value = null;
tab[i] = null;
--this.size;
} else {
int h = k.threadLocalHashCode & len - 1;
if (h != i) {
for(tab[i] = null; tab[h] != null; h = nextIndex(h, len)) {
}
tab[h] = e;
}
}
}
return i;
}
为了理解上述代码中的for循环需要理解threadLocal上entry存储的规则。逻辑如下:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = this.table;
int len = tab.length;
int i = key.threadLocalHashCode & len - 1;
for(ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = (ThreadLocal)e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
this.replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++this.size;
if (!this.cleanSomeSlots(i, sz) && sz >= this.threshold) {
this.rehash();
}
}
首先获取一个i,用来表明key在数组上的地址,如果该地址上的entry为null则跳过for循环直接进行赋值操作。如果该地址上已经存在entry,则进入到for循环进行逻辑处理。
在for循环中如果发现entry上的key和需要存储的key是同一个则重置value即可,如果发现已经被回收了进行相应替换操作。如果发现key和需要存储的key不一致,那么说明该地址已经被占用,需要进行再hash寻找地址,再hash确定地址逻辑也很简单就是寻找相邻的地址即可,最终直到成功存储key到数组上。
理解上述过程后再看expungeStaleEntry清除过程会更好理解,也就是说为了防止内存泄露,threadLocal会在代码内部的关键操作上通过调用expungeStaleEntry来防止泄露问题,但是expungeStaleEntry并不是每次都遍历全部的数组元素,这个看expungeStaleEntry逻辑很容易理解。所以为了及时清除不使用的value,最后在线程结束调用时,主动调用一次threadLocal的remove方法释放内存。
早期的threadLocal是可能存在内存泄露问题的,entry上的key是一个强引用,一旦threadLocal使用者忘记调用remove进行内存回收那么就会发生泄露问题,随着jdk不断完善后期才将key的引用修改为了弱引用。
WeakHashMap
如果理解了上述threadLocal的源码,再去看WeakHashMap的源码就会发现非常类似,不同的地址有两个
- entry的数据结构有所不同
- threadLocal是一个entry的数组,当发生hash冲突寻找下一个相邻地址,但weakhashmap是数组+链表的结构,和HashMap类似
看下entry的数据结构:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
......
}
多了一个ReferenceQueue,这个ReferenceQueue的作用就是当发生弱引用回收时会把对象放置到这个ReferenceQueue队列上,那么调用ReferenceQueue的poll方法就可以直观看到到底哪些弱引用被回收了,当进行内存清除时只需要遍历这个ReferenceQueue就可以完成清除工作,而threadLocal就需要无脑遍历数组确定到底哪些对象被回收了。
WeakHashMap清除逻辑
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
现在再回头去看WeakHashMap的代码,是不是就发现还真别说,和ThreadLocal还真的挺像的