一文带你读懂JDK源码:ThreadLocal类

2022-05-28 12:31:47 浏览数 (1)

线程封闭是实现线程安全的手段之一(另外的线程安全手段还有:使用并发工具类,可以参考)。

实现线程封闭的方法,就是今天的主角 -- ThreadLocal 类了;下面我们从4个角度剖析 ThreadLocal 类的源码:应用场景&功能、底层数据结构&源码、内存泄漏&规避手段 和 replaceStaleEntry()方法讲解

winter

本节涉及到 内部类&线程安全 的相关知识点,想了解更多这方面内容的同学,可以参考下面的文章列表:

  • 《Java并发编程实战》:第1章 多线程安全性与风险
  • 《Java并发编程实战》:第2章 影响线程安全性的原子性和加锁机制
  • 《Java并发编程实战》:第3章 助于线程安全的三剑客:final & volatile & 线程封闭
  • 《不清楚Java内部类的编译原理?读完这篇就够了》
  • 《带你读懂JDK源码:ThreadPoolExecutor》
  • 《带你读懂系统线程模型》

winter

另外一个基础内容是:ThreadLocal 哈希冲突的解决方法使用到了开放地址法,此处可以结合 HashMap 原理进行区分理解(HashMap 是通过链地址法解决hash 冲突问题,即:数组 链表 红黑树)。

(1)开放地址法是什么?

  • 基本思想:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
  • 缺点:
    • 容易产生堆积问题,不适于大规模的数据存储。
    • 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
    • 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

(2)ThreadLocalMap 采用开放地址法原因是什么?

ThreadLocal 源码有一个属性HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,容量长度 capacity 和 HASH_INCREMENT 与运算,得到的数组地址索引 index 往往能更加均匀的分布在2的N次方的数组里

由于ThreadLocal 往往存放的数据量注定不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。

应用场景&功能

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

下面我们通过一个例子,来了解 ThreadLocal 的使用:

在多线程环境下,ThreadLocal 变量被多线程同时访问并使用,验证它是否是线程安全的。

代码语言:javascript复制
public class ThreadLocalTest {
  public static void main(String[] args) throws InterruptedException {
    //主线程设置两个 ThreadLocal 变量
    final ThreadLocal threadLocal = new ThreadLocal();
    final ThreadLocal threadLocal2 = new ThreadLocal();
    threadLocal.set("test");
    threadLocal2.set(1111);

    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        //子线程1赋值(两个 ThreadLocal 变量)
        threadLocal.set("test 1");
        threadLocal2.set(1112);
        System.out.println("thread1 - threadLocal.value = "   threadLocal.get());
        System.out.println("thread1 - threadLocal2.value = "   threadLocal2.get());
      }
    });

    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        //子线程2赋值(两个 ThreadLocal 变量)
        threadLocal.set("test 2");
        threadLocal2.set(1113);
        System.out.println("thread2 - threadLocal.value = "  threadLocal.get());
        System.out.println("thread2 - threadLocal2.value = "  threadLocal2.get());
      }
    });
    t1.start();
    t2.start();

    Thread.sleep(1000);
    System.out.println("main - threadLocal.value = "   threadLocal.get());
    System.out.println("main - threadLocal2.value = "   threadLocal2.get());
  }
}

输出结果:

代码语言:javascript复制
thread1 - threadLocal.value = test 1
thread2 - threadLocal.value = test 2
thread2 - threadLocal2.value = 1113
thread1 - threadLocal2.value = 1112
main - threadLocal.value = test
main - threadLocal2.value = 1111

代码分析:

  • 例子1一共出现了 3 个线程:主线程、子线程1和子线程2,一共出现了 2 个 ThreadLocal 对象;这三个线程都分别对 ThreadLocal 对象进行了赋值,然后分别在自己的工作空间(堆栈)打印了 ThreadLocal 所赋值的内容。
  • 我们发现,虽然 三个线程都出现了对 ThreadLocal 相同对象的使用,但是最终打印结果都匹配上了线程内部操作结果,因此验证了“ThreadLocal 可以实现资源的线程封闭”。
  • ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

底层数据结构&源码

底层数据结构

ThreadLocal 比较特殊,它的内部并没有像 HashMap 等工具类那样自行维护一个存储数据的容器,而是提供了一个内部类定义给 Thread 类进行初始化引用,这个内部类就是 ThreadLocalMap 类。

所以我们剖析 ThreadLocal 底层,就是结合 Thread 类去理解 ThreadLocalMap 这个内部类所提供的能力(而这个内部类同样内部嵌套了另外一个内部类,那就是 Entry 类,不着急,慢慢来)。

我们先看看 ThreadLocalMap 内部类的源码:

代码语言:javascript复制
    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        /**
         * The number of entries in the table.
         */
        private int size = 0;
        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
}

分析源码:

  • 构造器(自不用多说)
  • 嵌套内部类 Entry (它是一个弱引用,可以参考文章《JDK提供的四种引用类型》)

为何使用Entry[]来维护每个数值,而不是使用HashMap这样键值对来存储数据?因为HashMap都是强引用,难以被清GC理,回收效率低。

  • 维护了一个数组容器:Entry[] table,它的每个元素类型大都是 嵌套内部类 Entry 对象;
  • 数组容器的初始化长度:INITIAL_CAPACITY = 16;

我们开头提到“ ThreadLocal 只是提供了一个静态内部类 ThreadLocalMap 给 Thread 类进行初始化引用”,因此我们需要了解下Thread的源码:

代码语言:javascript复制
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

分析源码:

  • 我们得出结论:每个线程内部单独维护了一个 ThreadLocal.ThreadLocalMap 的引用,而这个 ThreadLocal.ThreadLocalMap 内部核心是 Entry[] 数组,它是实现线程封闭的核心;
  • 那么这个Entry数组到底如何实现线程隔离的呢?且继续看下面的“常用API源码”小节。

小结:整理得到 ThreadLocal 的底层数据结构

一言蔽之,Thread 线程维护了一个 ThreadLocalMap,而 ThreadLocalMap 内部就是一个 Entry数组,该数组装的是 ThreadLocal 类对象和它所持有的值对象。

常用API源码

ThreadLocal 的三个最常用API,前面两个是 set(T value) 与 get(),分别实现赋值与获取值,remove()则是清理值。

代码语言:javascript复制
//获取ThreadLocal的值
public T get() { }
//设置ThreadLocal的值
public void set(T value) { }
//删除ThreadLocal
public void remove() { }

set(T value)

我们看下源码的 set(T value) 方法源码:

代码语言:javascript复制
    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @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 map = getMap(t);
        // map 是跟线程绑定的,初始化线程时,都是为null,
        // 不同线程不会出现共享map的情况,都会走到else部分的代码
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

分析源码:

源码的第一步:获取当前操作线程,并通过getMap(Thread t)拿到线程内部的成员变量 ThreadLocalMap ;

源码的第二步:判空Map是否存在,存在好办设置值完事,不存在就创建再设置值,此时Map的键值对为:key=当前ThreadLocal对象,value=传入值;

那么重点来了,此处的设置值相当考究,我们继续研究这两个代码段:

代码语言:javascript复制
createMap(t, value);
map.set(this, value);

(1)根据createMap() 方法,我们定位到 ThreadLocalMap 的 构造器:

代码语言:javascript复制
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
        /**
         * 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);
        }

代码分析:

  • createMap:就干一件事情,给当前线程的 ThreadLocalMap 成员变量初始化。
  • 第一步,初始化 ThreadLocalMap 内部维护的 Entry[] 数组,长度设置为 16;
  • 第二步,确定Entry 下标策略 = “当前ThreadLocalMap对象.hashCode 和 Entry[] 数组长度的 按位与运算结果”;
  • 第三步, 将(当前ThreadLocalMap对象,设置值对象)封装为Entry元素,并塞到数组中;
  • 第四步,设置 size 属性(长度) 以及 threshold 属性(扩容阈值);

(2)根据map.set() 方法,我们定位到 ThreadLocalMap 的 set() 代码块:

代码语言:javascript复制
        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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();
                // 可能1:如果传入的 ThreadLocal 对象相同,则直接覆盖之前的旧值
                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    // 可能2:如果 ThreadLocal 弱引用一句被清理掉了,那么需要替代它设置值
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

分析源码:

第一步,计算数组下标索引(策略跟上文说的一样)

第二步,遍历 Entry 数组,此处的循环成立条件为 e != null(元素冲突);

这里面临两种可能:

  • 其中一种是冲突 Entry 的弱引用 ThreadLocal 还没被GC清理掉(执行赋值操作);
  • 另一种是冲突 Entry 的弱引用 ThreadLocal被GC清理掉了(执行 replaceStaleEntry 操作,这部分源码有点复杂,我们单独拎出来讲解);

第三步,遍历完 Entry 数组,都没有发现有元素下标冲突的(执行赋值操作);

get()

我们看下源码的 get() 方法源码:

代码语言:javascript复制
    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();
    }

  private T setInitialValue() {
        // 初始化值为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // map 不为空,则直接设置对象值
            map.set(this, value);
        else
            // map 为空,则创建map再设置值
            createMap(t, value);
        return value;
    }

源码分析:

第一步,获取当前线程的 ThreadLocalMap 成员变量

第二步,ThreadLocalMap 里面寻址到 Key = ThreadLocal,直接返回;

第三步,如果 ThreadLocalMap 寻址失败,则执行 setInitialValue() 方法块;

remove()

我们看下源码的 remove() 方法源码:

代码语言:javascript复制
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

代码分析:

跟以上两个API比较,这部分内容相对简单,直接清理掉 ThreadLocalMap 的 Entry[] 数组的命中元素;

这个操作直接导致一个结果,那就是 ThreadLocal 对象的引用链会断开,这个 ThreadLocalMap 对象将会和 Thread 线程的 ThreadLocalMap 核心数组 Entry[] 不再有“瓜葛”。

那么,即使在线程池使用背景下,线程资源可以被反复利用于请求处理,而每次处理依赖的 ThreadLocal 对象将会被 GC 所回收,进而杜绝产生“内存泄漏”。

内存泄漏&规避手段

ThreadLocal 的 remove() 源码分析里所提到的:

  • 由于 Thread 单独维护了一个 ThreadLocalMap 核心数组 Entry[],所以产生的 ThreadLocal 对象会始终与 Entry[] 数组存在一个引用链的关系。
  • 由于 Entry元素 是弱引用,只有当GC发生时才会回收掉这部分资源,假如生产环境下JVM一直没有触发 GC 回收,那么会导致许多无效过期的 Entry元素 仍旧与 当前线程 Thread 存在引用链的关系。
  • 由于SpringMVC 使用线程池来处理请求的,当某个请求被处理完成之后,当前线程Thread不会立即被销毁掉(然后会重复利用在处理其他请求);

这就导致一个问题了:可能存在同一个线程的ThreadLocal数据被后面的请求使用(脏数据);

解决手段:我们需要在使用完ThreadLocal之后,进行一次remove()。

replaceStaleEntry()方法讲解

首先,我们定位到ThreadLocal 的 set() 方法源码:可以看到如果出现了某个Entry元素的 key 被JVM回收了,会出现 key 为null的情况,因此需要使用 replaceStaleEntry(key, value, i);

代码语言:javascript复制
       private void set(ThreadLocal<?> key, Object value) {
            // ...
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 数组该位置为空
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 数组该位置不为空, 此处调用了 replaceStaleEntry 方法
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // ...
}

代码分析:

  • 调用了 replaceStaleEntry() 方法的时机是Entry的Key过期被GC清理了。

下面我们走读下 ThreadLocal 的 replaceStaleEntry() 方法源码:(主要逻辑都在注释上了,确实要花些心思去理解,脉络就是)

代码语言:javascript复制
      /**
         * @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) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            // 以过期下标 staleSlot 为基准,i--,向前遍历
            // 直至找到 staleSlot 左边第一个为Null的元素结束
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            //以过期下标 staleSlot 为基准,i  ,向后遍历
            // 直至找到 staleSlot 右边第一个为Null的元素结束
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

              // 存在相同的key,所以需要(1)替换旧的值并且
              // (2)前面那个过期的对象的进行交换位置
                if (k == key) {
                    e.value = value;

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

                    //清理空节点
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    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.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

总结

这篇对 ThreadLocal 的源码分析有些冗长,前前后后也花了比较多时间去构图和思考源码的思路,一番思考下来,也顿悟了不少知识点,包括:哈希冲突、虚引用、线程封闭等。

其实,我们看源码目的不是去维护源码,而是通过阅读别人的代码来开拓思路,这样我们在日常写业务时,能更加注重细节的把握,写出高性能代码;希望文章内容能对大家有所帮助~

0 人点赞