【JUC】ThreadLocal的源码理解

引言
ThreadLocal提供了一种线程局部变量,在多线程环境下对同一个ThreadLocal对象的访问能够保证各个线程访问的是独立于其他线程的数据。
Thread类有一个ThreadLocal.ThreadLocalMap类的成员变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap,因此可以实现多个线程之间的线程隔离。

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

调用ThreadLocal对象的方法访问数据时,都会先调用Thread类的静态方法获取当前线程,然后获取当前线程的ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,该变量内部有一个Entry数组类型的成员变量table。
ThreadLocalMap的哈希机制
每个ThreadLocal实例在哈希表中的位置通过int i = key.threadLocalHashCode & (table.length - 1)计算得到。而key.threadLocalHashCode在创建ThreadLocal对象时通过执行private final int threadLocalHashCode = nextHashCode(),在private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}静态方法中初始的threadLocalHashCode从哈希增量0x61c88647开始,每个ThreadLocal对象创建并占用其哈希值后都会加上一个哈希增量,这种方法可以使hash更加均匀。如果发生了哈希冲突,ThreadLocalMap采用的是线性探测法。
扩容发生在set时,如果在set()中添加了一个新桶,那么添加完后会执行一次启发式清理,如果没有清理出任何过期桶并且当前哈希表已经满了则会rehash,在rehash()中会从头到尾清理连续式过期桶,如果清理后容量仍然大于0.75则会真正进行扩容,扩容大小*2

// java.lang.ThreadLocal.ThreadLocalMap.rehash()
private void rehash() {
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

set(T value)

// java.lang.ThreadLocal.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);
    }
}

在set(T value)中,如果当前线程的ThreadLocalMap为null,会新建一个map并将数据value放入,否则会调用map.set(this, value)。

// java.lang.ThreadLocal.ThreadLocalMap.set()
private void set(ThreadLocal<?> key, Object value) {
    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)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

根据key.threadLocalHashCode & (len-1)计算初始桶的位置,若初始桶不符合则以线性探测法向后尝试查找目标桶。在探测过程中:
1.如果探测到桶中的e.get() == key,那么直接替换。
2.如果探测到了一个空桶,那么new Entry(key, value)放入其中,接着执行一次启发式清理,如果启发式清理没有清理出任何过期桶并且当前哈希表已经满了那么执行rehash
3.如果探测到了一个过期桶e.get() == null,说明当前桶上Entry的key已被gc回收,那么从这个过期桶开始执行replaceStaleEntry()方法清理并最终把目标桶放在当前位置。

// java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null) slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

replaceStaleEntry()的清理过程中,首先从入口过期桶staleSlot开始前向连续查找,更新过期桶的开始下标slotToExpunge直到遇到空桶,此时开始下标slotToExpunge为整个前向连续区域中的第一个过期桶。
接着从入口过期桶staleSlot开始后向连续查找,如果后向连续区域中有过期桶并且前向连续区域中没有过期桶,那么将过期桶的开始下标slotToExpunge替换成后向连续区域中第一个过期桶的位置。
如果后向连续查找过程中发现k == key找到目标桶,那么更新数据并将目标桶与入口过期桶交换位置。如果slotToExpunge == staleSlot,说明在入口处的左右两侧连续区域只有入口处是过期桶,那么就从交换后的过期桶位置调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len),否则从第一个过期桶的开始下标slotToExpunge调用。
如果后向连续查找过程中遇到空桶,那么可以认为ThreadLocalMap中没有目标桶,那么替换掉入口处的过期桶并tab[staleSlot].value = null。如果slotToExpunge != staleSlot说明连续区域中除了入口处还有其他过期桶需要清理,那么从第一个过期桶的开始下标slotToExpunge调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)
expungeStaleEntry(slotToExpunge)中从开始下标,对于过期桶将其设为null,对于非过期桶则rehash
cleanSomeSlots()是启发式清理Heuristically scan some cells looking for stale entries.,其中可能包含多次expungeStaleEntry(slotToExpunge)
get()
在get()中,如果当前map中不存在对应的entry,那么调用initialValue()获取ThreadLocal实例设定的对应初始值并将entry放入map中。

// java.lang.ThreadLocal.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();
}
// java.lang.ThreadLocal.setInitialValue()
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

弱引用
当外部其他对ThreadLocal实例的引用被释放后,如果ThreadLocalMap中的Entry仍然持有对ThreadLocal实例的引用,由于线程存活时间可能较长因此会导致ThreadLocal实例无法被即时gc回收,而且在set触发的清理过程中e.get()返回的不是null,因此还会导致map更有可能扩容。
所以使用弱引用,只要外部其他的引用释放后,ThreadLocal实例可以即时被gc回收,避免内存泄漏。此外,没有外部其他引用说明ThreadLocal实例不再需要,那么与之对应的Entry也需要被回收,否则也会导致内存泄漏,还可能导致脏数据问题。对于脏Enrty,需要显式调用remove方法,否则只能在set()get()才会被清理。

// java.lang.ThreadLocal.ThreadLocalMap.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;
        }
    }
}

inheritableThreadLocals
ThreadLocal在异步场景下无法让子线程共享父线程中的线程副本数据。Thread类中还有另一个成员变量ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;,通过inheritableThreadLocals可以将创建子线程时父线程中的inheritableThreadLocals的副本拷贝给子线程。
Thread类的构造器中会调用init(),在init()中会实现父线程数据副本的深拷贝。
此外,阿里巴巴开源的TransmittableThreadLocal组件可以实现数据引用的浅拷贝。

posted @ 2025-07-04 02:15  hzx1011  阅读(17)  评论(0)    收藏  举报