浅析ThreadLocal知识点

445 阅读2分钟

ThreadLocal的概念

Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。

Thread、ThreadLocal关系

引用关系

Thread -> ThreadLocalMap -> entry -> (key,Object)

ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值会变成null,但是Entry是强引用,那么Entry里面存储的Object,并没有办法进行回收,所以ThreadLocalMap 做了一些额外的回收工作。

// java/lang/Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocal -> ThreadLocalMap 
// 继承WeakReference,key是弱引用 value 强引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用导致内存泄漏,为什么key不设置为强引用

key是threadlocal,把这个设置为弱引用,就是为了防止用户threadlocal=null的时候,key内存泄露,这个是为了解决key的内存泄露,但是(null,value)的情况存在。

如果key设置为强引用,当threadLocal实例释放后,threadLocal=null, 但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal,这样会导致threadLocal不能正常被GC回收。

弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行额外擦除操作。

// java/lang/ThreadLocal.java -> ThreadLocalMap
// 获取threadLocal值
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // key没有直接定位到值, 调用 getEntryAfterMiss -> expungeStaleEntry
        // getEntryAfterMiss()方法,在这个方法中,如果k == null , 则调用expungeStaleEntry(i)
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

// key=null,执行清理操作
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;
}

// key=null的Entry,value进行赋空值,释放空间避免内存泄漏,等待GC
// 重新hash,知道遇到Entry=null
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;
            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;
}

set 是在rehash的时机进行额外回收工作

线程执行结束后会不会自动清空Entry的value

当currentThread执行结束后, threadLocalMap GC不可达从而被回收,Entry等也就都被回收了。但是项目中经常使用线程池,所以currentThread一般不会处于终止状态。

实践

在不使用的情况下,主动调用remove方法进行数据清理。

引用类型介绍

引用类型GC回收时间用途生存时间
强引用never对象的一般状态JVM停止运行时
软引用内存不足对象缓存内存不足时终止
弱引用GC时对象缓存GC后终止
虚引用unknownunknownunknown