JUC ThreadLocal原理,通过源码进行学习深入了解

本文深入探讨了ThreadLocal的实现原理,解释了如何使用ThreadLocalMap为每个线程提供独立的变量副本,避免线程间的数据冲突。文章详细分析了ThreadLocalMap的内部结构,包括Entry的实现、哈希碰撞解决方案、垃圾回收机制以及线性探查策略。

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

概述

变量值的共享可以使用public static 变量的形式实现,所有的线程都是用同一个public static变量,那如何实现每一个线程都有自己的变量呢,jdk提供的ThreadLocal就是用于解决这样的问题。
ThreadLocal类提供了线程局部 (thread-local) 变量。这些变量与普通变量不同,每个线程都可以通过其 get 或 set方法来访问自己的独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

整体关系

在这里插入图片描述

基本原理

ThreadLocal中的嵌套内部类ThreadLocalMap,这个类本质上是一个map,和HashMap之类的实现相似,依然是key-value的形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用(之后会详细说到)。
通过 ThreadLocal 在指定的线程中存储数据。数据存储过后,只有该线程才能获取到数据,其他线程无法获取数据。
实现上述目的的 ThreadLocal 依赖的是 ThreadLocalMap。每个线程内部绑定一个 ThreadLocalMap,在此类的映射中,将 ThreadLocal 对象作为 key,将希望存入的值作为 value。
在这里插入图片描述

关键内部类ThreadLocalMap

此 map 和 HashMap 一样使用 Entry 作为 key-value 的存储结构。和 HashMap 不同的是,Entry 继承自 WeakReference 弱引用,用于在线程被销毁的时候,对 key进行垃圾回收。
发生哈希碰撞时,HashMap 使用双向链表和红黑树组织桶内数据结构,而 ThreadLocalMap 使用线性探查解决。
ThreadLocalMap 会频繁检查 table 数组中失效的 Entry,即被回收后key为null的entry,并进行垃圾回收和重新整理。

Entry

   /**
         * entry 继承自 WeakReference,使用 ThreadLocal 作为 key。
         * key 为 null(entry.get() == null)表示 key 不再被引用,entry 可以从
         * table 里删除了。
         *
         * Entry 继承 WeakReference,使用弱引用,可以将 ThreadLocal 对象
         * 的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以
         * 使 ThreadLocal 在没有其他强引用的时候被回收掉,这样可以避免因为
         * 线程得不到销毁导致 ThreadLocal 对象无法被回收。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            // key 对应的 value
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k); //使用k在底层创建了一个弱引用
                value = v;
            }
        }

super(k)会调用弱引用类中的方法,将内部referent的值设置为k。value则为简单的赋值,为强引用。这也是频繁删除失效entry的原因

成员变量

 /**
         * 初始容量,必须是 2 的幂。
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的数组,每个元素为 entry。
         * 必要时扩容。
         * table 的长度总是 2 的幂。
         */
        private ThreadLocal.ThreadLocalMap.Entry[] table;

        /**
         * table 中 entry 的数量
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * 扩容的阈值。设置为2/3 的 table.length
         */
        private int threshold; // Default to 0


构造函数

第一个构造函数在首次创建Thread中的threadLocals为null时,创建新Map时候会调用的,
第二个是createInheritedMap方法中会使用的,该方法在Thread的初始化函数中用于继承父类的inheritableThreadLocals使用。

  /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

工具类方法

setThreshold、 nextIndex、prevIndex

/**
         * 将扩容阈值设置为长度的 2/3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 计算下一个位置。table 可看成环形数组,当到达数组最后一个索引时,
         * 需要回到数组头部。
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 计算前一个位置。table 可看成环形数组。
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

setThreshold将阈值设置为 table 数组长度的 2/3,而 table 数组长度保持为 2 的幂。

为了符合线性探查的要求,将 table 作为循环数组,所以利用nextIndex和prevIndex方便计算下一个索引和上一个索引。


expungeStaleEntry

 /**
         * 通过 rehash 位于 staleSlot 和下一个空槽之间的任何可能碰撞的 entry,
         * 删除无效的 entry。还将删除到 null 槽之前遇到的其他无效 entry。
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
     
        private int expungeStaleEntry(int staleSlot) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;

            // 删除 staleSlot 位置的无效 entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            ThreadLocal.ThreadLocalMap.Entry e;
            int i;
            // 从 staleSlot+1位置 开始向后扫描一段连续的 entry,遇到空槽停止
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();//得到entry的软引用key
                // 如果遇到的 key 为 null,表示无效 entry,进行清理
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 遇到的 key 不为 null,计算索引
                    int h = k.threadLocalHashCode & (len - 1);  //计算key不为null时key的散列值
                    // 计算出来的索引 h 与当前的索引 i 不一致,从计算出来的索引
                    // 开始,向后查找到第一个空槽,把当前 entry 移动到其正确的
                    // 位置。同时将 i 处置为 null。
                    if (h != i) { //如果当前散列值不为i,说明第一次散列时位置上存在值,只是向后移动找空位填造成的
                        tab[i] = null;   //就将当前桶赋值为null,方便GC

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        //然后将正确的散列值h放到对应位置上,如果发现被占用,
                        //就向后移动,直到找到空桶进行填入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            // 返回遇到的空槽的索引。即table[i]==null
            return i;
        }

方法概括为:

  • 首先删除table中stateSlot这个桶上的entry,进行回收
  • 然后将指定的参数staleSlot代表的桶位置作为初始遍历起点
  • 直到遇到之后的第一个空桶为止,才停止循环,即遇到table[i]=null停止
  • 在遍历过程中,将遇到的每一个entry的key进行判断,
  • 如果key为null就重置entry为null,方便GC,
  • 如果key不为null,则对这个entry进行重新散列,散列规则和前面一致,用key的hashcode做与运算得到的正确位置的h,如果桶h不为空,则填充到之后的第一个空桶中。

replaceStaleEntry

   /**
         * 替换无效的 entry。
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
            ThreadLocal.ThreadLocalMap.Entry e;

            // 由于使用的是线性探查,所以需要向后查找和向前查找,确保 entry
            // 放在最前面的空桶里,确保清除了所有的无效 entry


            // 根据转入的无效 entry 的位置(staleSlot),向前扫描一段连续的
            // entry。直到遇到第一个为null的桶
            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).

            int slotToExpunge = staleSlot;//记录要开始删除的无效桶位置的起点

            //首先需要从传入staleSlot-1开始向前遍历
            //此循环只会在遇到第一个为null的桶时结束,
            //所以slotToExpunge记录的是循环的最后一个放的是无效entry的桶的索引
            //即理解为,这个索引到staleSlot之间至少存在一个无效entry。
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                // 如果是无效的,更新 slotToExpunge 记录此时的索引
                if (e.get() == null)  //entry的key为null,说明无效。
                    slotToExpunge = i;

              //注意从上面循环结束的 slotToExpunge,只有在没被更新的情况下,
            //下面这个循环才会去设置slotToExpunge。
            // 即如果slotToExpunge!=staleSlot,那么经过下面的循环slotToExpunge也不会改变了

            // 从 staleSlot+1 开始向后遍历,直到遇到第一个为null的桶,或满足条件
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get(); //得到entry的key

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                // 如果找到了 key,把 key 对应的 value 设置成指定的 value。
                if (k == key) {
                    e.value = value;  //覆盖值

                    // 把 stableSlot 无效的引用转移到索引 i 位置,然后将
                    // stableSlot 位置设置成有效的指定 key-value
                    //即根据传入的key找到的对应entry和传入的staleSlot上的无效桶上的entry交换。
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    // 如果向前查找没有找到无效的 entry,并且查找过程中也没有遇到无效entry
                    // 则更新 slotToExpunge为当前的i。
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;  //将要开始删除的位置赋值为交换后无效entry的桶位置i
                    //使用更新的slotToExpunge进行expungeStaleEntry,
                    // 即会删除当前位置开始到后面出现的第一个null值的桶之间的所有无效的entry,
                    //并将有效的entry进行重新散列。
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                // 如果向后扫描过程中,还没遇到相等的key之前遇到了无效entry,
                // 且之前向前扫描中没有找到无效的 entry,
                // 则更新 slotToExpunge 为当前的 i,
                //注意更新完staleToExpunge后,下一次运到无效entry不需要更新了
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 如果向前扫描找到了无效的 entry,则 slotToExpunge 不会变
            }

            // 经过向前和向后查找之后,若 staleSlot 位置的 value 为空,表示
            // key 之前不存在,则直接新增一个 entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

            // slotToExpunge 不等于 staleSlot,说明有无效的 entry 需要清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
  • 该方法除了替换,还会包括一个向前遍历和一个向后遍历的循环
  • 除了找到对应的key进行替换后,
  • 还会确定一个slotToExpunge,这个slotToExpunge的优先级一定是:staleSlot-1向前遍历找到的索引>staleSlot+1 向后遍历还未找到对应entry时遇到的无效entry的slot索引>最后找到对应entry后进行交换后的slot索引。
  • 从slotToExpunge开始调用一次expungeStaleEntry。
  • 并从返回的值为null的桶的索引开始再进行一次cleanSomeSlots。

cleanSomeSlots

    /**
         * 启发式扫描一些单元格,寻找无效的 entry。这是在添加新元素或删除
         * 另一个无效元素时调用的。它执行 log 级次数的扫描,以平衡
         * 无扫描(快速但保留垃圾)和与元素成比例的扫描次数,后者将会找到
         * 所有垃圾但是会导致一些插入花费 O(n) 时间。
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;//table长度
            do {
                i = nextIndex(i, len); //取到下一个索引
                ThreadLocal.ThreadLocalMap.Entry e = tab[i]; //取到下一个索引对应桶上的entry
                if (e != null && e.get() == null) {   //如果entry不为null,且entry.key为null,说明entry过期
                    // 如果遇到过期entry,则重置n,说明又要进行log2(n)次的遍历
                    n = len;
                    removed = true;
                    // 调用 expungeStaleEntry 删除无效的 entry
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0); //>>>无整数右移,即n/2,
            // 如果进行过删除无效 entry 的操作,返回 true
            return removed;
        }

注意以上方法中判断循环的条件是 (n >>>= 1) != 0,但是n值会在遍历到key为null的无效entry时进行重置。

expungeStaleEntries

该方法比较简单,就是遍历底层的table,如果遇到key为null的无效entry对这个桶索引调用expungeStaleEntry方法进行清理。

/**
         * 清除 table 中所有的无效 entry。
         */
        private void expungeStaleEntries() {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                ThreadLocal.ThreadLocalMap.Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

getEntry

 /**
         * 获取与 key 关联的 entry。此方法是快速查找:直接命中存在的 key,
         * 否则将会跳转到 getEntryAfterMiss。这是为了最大限度提高直接命中
         * 的性能设计的,部分原因是为了使这种方法线性化。
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            ThreadLocal.ThreadLocalMap.Entry e = table[i];
            if (e != null && e.get() == key)    //如果找到的桶位置上不为null,key也相等,那么直接取出当前entry
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 当直接查找索引没有找到该 key 时的 getEntry 版本。
         *
         * @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 ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;

            //从上面找到的key散列后本应放的位置开始遍历
            //过程中遇到entry如果key为null,则会调用expungeStaleEntry从i开始删除过期条目到下一个空桶为止
            //如果遇到key相等的,就直接返回这个entry,否则遍历到第一个空桶为止。
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                // 清理无效的 entry
                if (k == null)
                    expungeStaleEntry(i);
                    // 线性探查向后查找
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;    //没在循环中返回,说明没找到。返回null
        }
  • 根据传入的key,进行一次散列,得到散列值i,如果桶i中,也就是e=table[i]!=null,就接着判断e.get()获得的key
  • 如果key也相等,说明直接命中,就返回e,如果不相等,则需要进一步在getEntryAfterMiss中判断
  • 从上面找到的key散列后本应放的位置i开始遍历,循环正常终止的条件为下一个空桶为止。
  • 在遍历过程中,如果遇到key=null的无效entry还会调用expungeStaleEntry进行清理。
  • 如果遇到entry的key等于传入的key时,就返回e。
  • 如果循环结束还没返回,说明没有找到,则返回null。

set

  /**
         * 将指定 key 对应的 value 设置成指定 value。
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            ThreadLocal.ThreadLocalMap.Entry[] tab = 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)]) {

                // 获取 entry 的 key
                ThreadLocal<?> k = e.get();

                // 如果 key 就是指定的 key,即找到了其所在 entry,设置 value 值后返回
                if (k == key) {
                    e.value = value;   //覆盖
                    return;
                }
                // 如果 k 为 null,说明被回收了,该位置可以使用,使用新的
                // key-value 替换
                // 此时可能还没有找到 key,key 可能存在数组后面的位置
                if (k == null) {
                    //该方法除了替换,还会包括一个向前遍历和一个向后遍历的循环
                    //除了找到对应的key进行替换后,
                    //还会确定一个slotToExpunge,
                    //从slotToExpunge开始调用一次expungeStaleEntry,
                    //并从返回的值为null的桶的索引开始再进行一次cleanSomeSlots。
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果没找到传入的key,则新建entry。
            tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
            int sz = ++size;    //size自加

            // cleanSomeSlots 清除 table[index] != null && table[index].get() == null
            // 的对象。这种 key 关联的对象已经被回收了。
            // 如果没有清除任何 entry,且使用量达到了阈值,则 rehash 扩容。
            //注意rehash会先使用expungeStaleEntries删除table中所有的失效entry后
            //再使用size和table的一半容量比较,如果size大于等于length/2,才会进行resize.
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
  • for循环从传入的key经过计算应该散列到的桶的索引i开始遍历,退出条件为直到遇到第一个为null值的table[i]
  • 遍历过程中,如果entry的key和传入的key相等,会进行value的值覆盖,并返回
  • 如果entry的key为null,则会调用replaceStaleEntry进行entry的整个覆盖。
  • 在replaceStaleEntry方法中也会删除table中无效的entry,方法调用结束后,set会返回
  • 如果以上条件没有返回,说明table中没有找到传入的key,那么就在循环结束的空桶位置新建entry放入,size自加。
  • 然后调用一次cleanSomeSlots进行删除无效entry,如果此次清楚删除掉了一些无效entry,那么不进行扩容判断。
  • 如果此次cleanSomeSlots没有清楚掉任意无效entry,那就将当前size和threshold进行比较
  • 如果table中包含的键值对数量超过了threshold就进行rehash方法,进入扩容方法中。

remove

  /**
         * 删除指定 key 对应的 entry。
         */
        private void remove(ThreadLocal<?> key) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = 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)]) {
                if (e.get() == key) {
                    // 调用 WeakReference 的 clear 方法设置key值为null。
                    e.clear();
                    // 连续段内清除无效 entry
                    expungeStaleEntry(i);  //expungeStaleEntry方法从当前i位置清楚无效entry
                    return;
                }
            }
        }
  • 对传入的key进行散列运算,得到i.
  • 从桶i开始遍历,直到遇到一个空桶null为止
  • 遍历过程,直到key相同的entry,调用软引用删除方法clear将内部key赋值为null,
  • 然后调用expungeStaleEntry,从当前i开始到后面第一个为null的桶进行一次无效entry清理。

扩容方法

注意rehash并不是一定扩容,而是先调用一次expungeStaleEntries,清楚掉table中所有无效的entry,并让元素重新散列。
然后再判断删除后的键值对数量size 和1/2的table长度len比较,如果满足条件,就进行resize扩容。

  • resize扩容方法中,新table容量扩大为老容量的两倍,所以它仍然是2的幂。
  • 遍历旧表的过程中,再一次判断遍历到的entry是否key为null
  • 如果为null,则令其value=null,方便GC。
  • 如果,entry有效,则先计算散列索引,在线性检查插入新列表的空桶中,并且count计数加1
  • 最后对threshold和size和table进行赋值,完成扩容。
  /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries(); //先清楚table中所有的无效entry,且所有元素都经过重新散列。

            // Use lower threshold for doubling to avoid hysteresis
            // 全部清理过后 size 减小了,所以判断如果 size 大于 len / 2 即扩容
            //桶中元素个数如果超过了table长度的一半就进行扩容。
            //threshold=2/3len,threshld/4=1/6len,所以 threshold - threshold / 4= 1/2 len
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * table 的容量扩大为原来的两倍(仍然为 2 的幂)
         */
        //resize过程中也会删除掉过期的entry
        private void resize() {
            ThreadLocal.ThreadLocalMap.Entry[] oldTab = table; //得到旧表
            int oldLen = oldTab.length;   //得到旧表长度
            int newLen = oldLen * 2;   //乘2进行扩容
            //使用新容量作为指定长度,创建存放entry的新表
            ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
            int count = 0; //存放有效值,即entry中失效的不会被记录

            for (int j = 0; j < oldLen; ++j) {
                ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    // 虽然做过清理,但扩容过程中数组动态变化,可能又存在 k == null
                    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++;
                    }
                }
            }

            // 设置新的阈值,为当前容量的2/3
            setThreshold(newLen);
            size = count; //size赋值为有效值count
            table = newTab;   //将底层数组进行赋值
        }

ThreadLocal关键方法

构造函数即工具方法

ThreadLocal只有无参构造函数
getMap接收一个Thread对象,返回线程中的threadLoals记录的Map。
initialValue方法用户可以重写,用于指定get方法如果Map为null时,设置的初始vlaue。
setInitialValue在线程Map为空情况下调用ThreadLocal.get时调用的初始化Map。
createMaps调用ThreadLocalMap的构造函数,对当前线程的Map进行创建赋值。

   /**
     * 构造函数
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }
	
	ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;  //每个线程中都存放了属于自己的ThreadLocal.ThreadLockMap。
    }
    /**
     * 为这个 thread-local 变量返回当前线程的初始 value。此方法在线程第一次
     * 使用 get 方法访问变量时调用,除非线程之前使用了 set 方法。
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }
  /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        // value 默认为 null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // map 不为 null 表示已经初始化,则调用 set 设置 value 即可
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
/**
     * 创建和 ThreadLocal 关联的 map
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

get

在线程执行过程中,调用某个 ThreadLocal 的 get 方法,即获取此线程和该 ThreadLocal 绑定的 value 值。

首先获取该线程的 ThreadLocalMap 变量,然后在获取到的 map 中,根据指定的作为 key 的 ThreadLocal,查找其对应的 value 并返回。如果Map为null,或者没有找到该key对应entry,则进行初始化(延迟初始化模式)。

  /**
     * 返回当前线程的 tread-local 变量副本的值。如果当前线程没有该值,首先
     * 将其初始化为调用 initialValue 返回的值。
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        // 获取当前线程的 ThreadLocalMap 实例
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // map 为空则初始化,或者entry没有找到
        return setInitialValue();
    }
   


set

调用 ThreadLocal 的 set 方法,将 value 和指定的线程绑定。在具体的实现方法中,和 get 类似,首先获取该线程的 ThreadLocalMap 对象,若该 map 存在则调用 map.set 方法设置 value,否则创建新的 map 作为 ThreadLocalMap。

 /**
     * 设置当前线程的 thread-local 变量副本为指定的 value。大多数子类不需要
     * 重写此方法,仅仅依靠 initialValue 来设置 value。
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程持有的 ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 如果持有则设置 Map 中此 ThreadLocal 对应的 value
        if (map != null)
            map.set(this, value);
            // 否则创造一个新的 Map 并设置值
        else
            createMap(t, value);
    }

remove

调用 ThreadLocalMap 的 remove 方法,删除 key 为此 ThreadLocal 的键值对。

 /**
     * 删除当前线程的 value。
     *
     * @since 1.5
     */
    public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

为什么 key 使用弱引用以及可能产生的内存泄露

如果使用强引用,当 ThreadLocal 对象的强引用被回收了,ThreadLocalMap 本身还持有 ThreadLocal 的强引用(因为 ThreadLocalMap 的 key 指向 ThreadLocal)。如果没有手动删除这个 key,ThreadLocal 就不会被回收。只要线程不消亡,ThreadLocalMap 引用的对象就不会被回收。可以认为这导致内存泄露(本该回收的无用对象没有被回收)。

如果使用弱引用,当 ThreadLocal 的强引用被回收了,就只剩下 ThreadLocalMap 持有的弱引用对象了,在下一次 gc 的时候,这个 ThreadLocal 就会被回收。但是 ThreadLocal 只是 key,其对应的 value 不是弱引用,不会被回收,当 key 变成 null 之后,value 再也无法被访问到,内存泄露依然存在。

这就是上面 expungeStaleEntry、cleanSomeSlots、expungeStaleEntries 方法存在的原因。每次 get/set/remove ThreadLocalMap 中的值的时候,会自动清理 key 为 null 的 value。

那为什么不对 value 使用弱引用呢?因为 value 在其他地方没有保留任何强引用,如果在这里使用弱引用,将会在任何一次 gc 的时候被回收,很明显这样是不对的。

强引用对象是指不会被回收的对象;软引用对象是指内部不足的时候回收的对象;弱引用对象是指存活到垃圾回收前的对象,此类对象在垃圾回收发生时立刻进行回收。

解决方法

如果 ThreadLocal 的强引用一直存在,只要线程不死,ThreadLocalMap 里的 key-value 将会一直存在(value 将会一直存在),因为无法通过弱引用来删除。

所以当某个 ThreadLocal 不再使用时,最好使用 ThreadLocal.remove 删除键值对。

参考

ThreadLocal源码分析
ThreadLocal原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值