ThreadLocal源码解析

本文深入解析ThreadLocal的工作原理,包括其内部角色关系、内存泄漏问题、Hash索引机制及线程安全性。通过源码分析,详细阐述get、set、remove等方法的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是ThreadLocal?它是属于线程自己的小仓库。关于它的使用与介绍看这一篇:ThreadLocal的介绍与使用

关于ThreadLocal的原理,理清四个角色关系:Thread,ThreadLocal,ThreadLocalMap,Entry:
​​​​​​在这里插入图片描述
在Thread中有个变量指向ThreadLocalMap

    ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal的静态内部类,当线程第一次执行set时,ThreadLocal会创建一个ThreadLocalMap对象,设置给Thread的threadLocals变量。
静态内部类常称为嵌套类,表明内部类对象不需要与其外围类对象之间的联系,也就是不持有外围类对象的this引用。

    static class ThreadLocalMap {

Entry是ThreadLocalMap的静态内部类,继承自WeakReference,它对ThreadLocal的引用是弱引用,是实际存储ThreadLocal对象 到 线程局部变量 之间映射的容器。

        static class Entry extends WeakReference<ThreadLocal<?>> {

一个ThreadLocalMap种储存多个Entry

        private static final int INITIAL_CAPACITY = 16;

        private Entry[] table;

一段程序可创建多个ThreadLocal对象,但线程Thread对象只持有一个ThreadLocalMap对象,不同ThreadLocal映射到 table 数组不同位置。

各角色关系图(虚线代表弱引用)
在这里插入图片描述

关于内存泄漏
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

虽然EntryThreadLocal的引用是弱引用,但是线程的栈中持有对ThreadLocal的强引用,而当对象只存在弱引用时才会被回收,这就存在OOM的风险,有两种解决办法:1,主动将程序中对ThreadLocal对象的引用置为null,这样该ThreadLocal对象以及所有它的Entry都将被清理,当然你可以创建多个ThreadLocal对象,但是这并不是个好的解决办法,比如你还想之后继续使用该ThreadLocal对象。2,主动调用ThreadLocal#remove方法,清除当前线程的所有键值对,该方法会主动将EntryThreadLocal的弱引用置为nullkeynullEntry会被清理。

关于Hash索引

采用hash来定位线程存储的的value。

ThreadLocalMap初始化时会创建大小为 16 的Entry[] table 数组

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

关于位置的确定:key.threadLocalHashCode & (table.length - 1);
ThreadLocal 的 threadLocalHashCode

    private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    private static final int HASH_INCREMENT = 0x61c88647;

使用一个 static 的原子属性 AtomicInteger nextHashCode,确保不同ThreadLocal对象拥有相同的AtomicInteger对象,目的是将不同ThreadLocal的映射数据均匀分散开,分散间隔由 HASH_INCREMENT = 0x61c88647控制 ,如大小为 16 的数组,第一个ThreadLocal对象在 0 位,第二个在 7 位,第三个在 14 位置,第四个为5…每次间隔为 7。
位置的计算 threadLocalHashCode & (INITIAL_CAPACITY - 1)

如何处理 hash 冲突?
采用线性探测法来处理冲突,从当前位置往后找寻空位,空位指的是table[ i ] = null 或是 table[ i ] .key = null,将Entry插入该位置。也就是说一个Entry要么在它的hash位置上,要么就在该位置往后的某一位置上。

源码分析

get 方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // 返回当前线程持有的ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

分为两步:
1,获取当前线程的ThreadLocalMap对象,不为空则调用它的getEntry方法来获取存储值。
2,若当前线程之前未调用 set 存储值,则调用 setInitialValue 方法:ThreadLocal的子类可以重写 initialValue 方法,并设置返回值,也就是你可以设置初始化值,这样在未 setget 会将该初始值存储进 table 数组中,并返回该初始值。

先来看看第一步getEntry:
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

1)计算该ThreadLocal对象在table中的存储位置,获取该位置的Entry
2) 不存在:分两种1,该位置为空,返回null;2,该位置Entrykey并非是当前的ThreadLocal对象,这是由于线性探测法的影响,调用 getEntryAfterMiss方法:该方法从 i 位置开始往后找寻 key 指向当前 ThreadLocal 对象的Entry,找到就返回,或遍历到空位时结束循环返回null;在找寻途中若发现 keynullEntry,调用 expungeStaleEntry 方法对该段 run 进行修正,由于线性探测发 table 数组中的情况一定是一段一段连续的片段,我们将一个连续的片段称为 run

        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);// 修正table数组
                else
                    i = nextIndex(i, len); // 即后一位
                e = tab[i];
            }
            return null;
        }

expungeStaleEntry : 作用是修正该段 run 片段,这里修正指的是:由于线性探测法的影响,Entry 可能并不在自己的 hash 位置上,比如一个Entry的hash 位置是 i 但实际它在后面的 j 位置,当我们在 i 到 j 之间发现某Entry 的key 为null,删除该 Entry,对后面的元素进行修正,也就是重新计算它们的hash 位置,目的是维持我们的线性探测数组 table 不被破坏,否则之后很可能会找不到Entry。这里并非是对后面所有的元素进行修正,它的范围是直到空位为止,这也很好理解。
可以看到重要的是你对线性探测法有一定的了解,推荐《算法》一书,里面有对hash冲突的解决办法分析:线性探测法 和 拉链法。

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 置空该位置
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 将该位置之后直到空位的元素重新rehash,修正位置
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 若发现废弃的Entry,置空该位置,继续rehash后面的元素
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                // 对该位置的Entry进行rehash,重新确认新的位置
                } 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;
        }
再来看第二步的 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);
        return value;
    }

将 initialValue 方法的返回值存储,若 ThreadLocalMap 对象为 null,则调用 createMap 创建。这里 initialValue 方法默认返回null,ThreadLocal 的子类可重写以设置初始值。

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
// ThreadLocalMap 的构造函数:创建大小为16的Entry数组,构造Entry放入数组,
// 更新阀值
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        // ThreadLocalMap#threshold
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取线程的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 调用ThreadLocalMap#set 构造Entry并插入table中
        if (map != null)
            map.set(this, value);
        // 为null则创建ThreadLocalMap 对象赋给线程
        // 并在初始化过程中构造Entry插入table中
        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); // 计算hash位置
			// hash冲突,往后找寻空位插入
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				// 已存在则跟新value
                if (k == key) {
                    e.value = value;
                    return;
                }
				// 发现废弃的Entry
                if (k == null) {
                	// 将key的entry插入到i位置,具体看下面分析
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value); // 构造Entry
            int sz = ++size;
            // cleanSomeSlots:从 i 往后探测table数组的情况,具体分析在下面
            // 该方法返回true,说面至少删除了一个废弃的Entry,也就是size小于等于之前
            // 返回false又发现size达到阀值,调用rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

1,发现重复,更新value
2,未发现重复前先发现了废弃的Entry,调用replaceStaleEntry,Entry会被插入到该位置。
3,不是上面两种情况,构造Entry插入到空位 i 位置上,判断是否需要扩容。

replaceStaleEntry: 将 key 的 Entry 插入到 staleSlot 位置,这里分两种情况:1,在 staleSlot 后发现相同的key;2,没有发现,构造新的 Entry。在过程中还会根据情况修正run,以及探测table数组情况并根据情况修正。

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
			// slotToExpunge 表示该run的第一个废弃Entry的位置,
			// 之所以要找第一个,目的就是为了之后调用 expungeStaleEntry
			// 修正该run片段之用
            int slotToExpunge = staleSlot;
            // 往前找
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 从staleSlot 往后遍历该run
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
				//找到相同的key
                if (k == key) {
                    e.value = value;//更新value
					// 将staleSlot位置的Entry与i位置对换
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 若想等则说明该run在i之前并无废弃的Entry,
                    // 由于上面的互换,该run的第一个废弃Entry位置变为了 i
                    // 所以将slotToExpunge置为i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // expungeStaleEntry:从slotToExpunge开始修正该run,
                    // 返回该run的结尾位置,结尾指的是为空的位置
                    // cleanSomeSlots:探测下一个run的情况,发现废弃就进行修正
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
				// 发现废弃Entry,若它是该run段的第一个废弃Entry,
				// 则更改slotToExpunge
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 若是未发现 key,则构造新Entry插入 staleSlot 位置
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 该run中还有废弃的Entry
            if (slotToExpunge != staleSlot)
            	// expungeStaleEntry 从slotToExpunge往后修正该run,
            	// 返回值代表该段run的结尾后的那个空位置下标
            	// cleanSomeSlots 从该位置往后检查下一个run片段的情况
            	// 并对其进行修正
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

cleanSomeSlots:正如方法的名字,该方法就是清理一些废弃的Entry,那么一些是多少? 每次循环都将 n 减半直到为0为止,该该过程中若未发现废弃Entry,则中止返回 false;若发现了则将 n 重置为 table.length,也就是发现一个就延长探测范围,去发现更多,最终返回true。

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len); // 下一位置
                Entry e = tab[i];
                // 发现废弃元素,重置n,延长探测范围
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true; // 表明删除过废弃的Entry
                    // expungeStaleEntry方法上面分析过,就是修正该run片段
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

rehash:首先清理table中所有的run片段,之后若仍需扩容的话,调用resize。

        private void rehash() {
            expungeStaleEntries();//对整个table数组中所有run进行修正

            // Use lower threshold for doubling to avoid hysteresis
            // 采用较低的阀值,影响线性探测法性能的便是run的长度,低阀值保证了
            // table 中不会出现过长的run。
            if (size >= threshold - threshold / 4)
                resize(); // 扩容
        }
        // 对整个table数组中所有run进行修正
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

resize:扩容操作,table 扩大为原来的两倍,扩容中不会将废弃的Entry移到新数组中。

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

remove删除操作

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

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) {
                	// 将Entry对ThreadLocalMap对象的引用置空,以便被GC回收
                    e.clear(); 
                    expungeStaleEntry(i);// 删除i位置并修正该run
                    return;
                }
            }
        }

关于线程安全性

每个线程都有自己的ThreadLocalMap,以及Entry[] 数组,只有自己操作,所以是线程安全的。那么ThreadLocal呢?它并没有可更改的状态,所以也是线程安全的,来看看它的三个成员变量

	// 每个ThreadLocal对象初始化后都会得到自己的hash值,之后不会再变
    private final int threadLocalHashCode = nextHashCode();
	// 静态对象AtomicInteger,与ThreadLocal对象无关,
	// 在第一次ThreadLocal类加载时初始化
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
	// 不可变
    private static final int HASH_INCREMENT = 0x61c88647;

所以说 ThreadLocal 也是线程安全的。

关于OOM的例子分析

推荐两篇博客
ThreadLocal造成OOM内存溢出案例演示与原理分析
借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference

关于引用队列

下面是引用 借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference这篇博客,因为之前没用过引用队列,所以在这里写下来,熟悉一下。

在很多场景中,我们的程序需要在一个对象的可达性(GC可达性,判断对象是否需要回收)发生变化的时候得到通知,引用队列就是用于收集这些信息的队列。

在创建SoftReference对象时,可以为其关联一个引用队列,当SoftReference所引用的对象被回收的时候,Java虚拟机就会将该SoftReference对象添加到预支关联的引用队列中。

需要检查这些通知信息时,就可以从引用队列中获取这些SoftReference对象。

不仅仅是SoftReference支持使用引用队列,软引用和虚引用也可以关相应的引用队列。

在这里插入图片描述
例子:

public class WeakCache {

    private void printReferenceQueue(ReferenceQueue<Object> referenceQueue) {
        WeakEntry sv;
        while ((sv = (WeakEntry) referenceQueue.poll()) != null) {
            System.out.println("引用队列中元素的key:" + sv.key);
        }
    }

    private static class WeakEntry extends WeakReference<Object> {
        private Object key;

        WeakEntry(Object key, Object value, ReferenceQueue<Object> referenceQueue) {
            //调用父类的构造函数,并传入需要进行关联的引用队列
            super(value, referenceQueue);
            this.key = key;
        }
    }

    public static void main(String[] args) {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        User user = new User("xuliugen", "123456");
        WeakCache.WeakEntry weakEntry = new WeakCache.WeakEntry("654321", user, referenceQueue);
        System.out.println("还没被回收之前的数据:" + weakEntry.get());

        user = null;
        System.gc(); //强制执行GC
        System.runFinalization();

        System.out.println("已经被回收之后的数据:" + weakEntry.get());
        new WeakCache().printReferenceQueue(referenceQueue);
    }
}

在这里插入图片描述

ReferenceQueue引用队列记录了GC收集器回收的引用,这样的话,我们就可以通过引用队列的数据来判断引用是否被回收,以及被回收之后做相应的处理,例如:如果使用弱引用做缓存则需要清除缓存,或者重新设置缓存等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值