秃头码哥 发表于 2021-8-14 13:30:23

详解ThreadLocal和ThreadLocalMap源码

媒介

在日常实现功能的过程当中,都会碰到想要某些 静态变量,不管是 单线程 亦大概是 多线程在使用,都不会产生相互之间的影响,也就是这个 静态变量 在线程之间是 读写隔离 的。
有一个我们常常使用的 工具类,它的并发问题就是用 ThreadLocal来解决的,我相信大多数人都看过,那就是 SimpleDateFormat 日期格式化的工具类的 多线程安全问题,各人去网上搜的话,应该会有一堆人都说使用 ThreadLocal。
正文

ThreadLocal的定义

ThreadLocal 的表意是 线程本地 的意思,用来存放可以或许 线程隔离 的变量,那就是 线程本地变量。也就是说,当我们把变量保存在ThreadLocal当中时,就可以或许实现这个变量的 线程隔离。
ThreadLocal的使用示例

我们先来看两个例子,这里也刚好涉及到两个概念,分别是值通报和引用通报。

[*]值通报






public class ThreadLocalTest {    private static ThreadLocal threadLocal = new ThreadLocal(){      protected Integer initialValue(){            return 0;      }    };    // 值通报    @Test    public void testValue(){      for (int i = 0; i < 5; i++) {            new Thread(() -> {                Integer temp = threadLocal.get();                threadLocal.set(temp + 5);                System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal.get());            }, "thread-" + i).start();      }    }}以上程序的输出效果是:
current thread is thread-1 num is 5current thread is thread-3 num is 5current thread is thread-0 num is 5current thread is thread-4 num is 5current thread is thread-2 num is 5我们可以看到,每一个线程打印出来的都是5,哪怕我是先通过 ThreadLocal.get()方法获取变量,然后再 set进去,依然不会进行重复叠加。这就是线程隔离。
但是对于引用通报来说,我们又必要多留意一下了,直接上例子看看。

[*]引用通报


public class ThreadLocalTest {    static NumIndex numIndex = new NumIndex();    private static ThreadLocal threadLocal1 = new ThreadLocal() {      protected NumIndex initialValue() {            return numIndex;      }    };    static class NumIndex {      int num = 0;      public void increment() {            num++;      }    }    // 引用通报    @Test    public void testReference() {      for (int i = 0; i < 5; i++){            new Thread(() -> {                NumIndex index = threadLocal1.get();                index.increment();                threadLocal1.set(index);                System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal1.get().num);            }, "thread-" + i).start();      }    }}我们看看运行的效果
current thread is thread-0 num is 2current thread is thread-2 num is 3current thread is thread-1 num is 2current thread is thread-4 num is 5current thread is thread-3 num is 4我们看到值不但没有被隔离,而且还出现了线程安全的问题。
以是我们一定要留意值通报和引用通报的区别,在这里也不讲这两个概念了。
源码分析

想要更加深入地了解 ThreadLocal 的作用,就必要检察它的源码实现,整个类加起来也不过七八百行而已。分为 ThreadLocal 和 ThreadLocalMap 两部门分析它的源码分析。下面是 ThreadLocal 和 ThreadLocalMap 以及 Thread 之间的关系。
https://p26.toutiaoimg.com/large/pgc-image/dca11cb1945a43f5a6ea2bc6febc67f1
ThreadLocalMap源码分析

ThreadLocalMap 在 Thread 类中作为一个成员属性而被引用。
public class Thread implements Runnable {    ThreadLocal.ThreadLocalMap threadLocals = null;}ThreadLocalMap 是 ThreadLocal 内里的一个 静态内部类,但是确实一个很关键的东西。
我盼望通过我的文章,不求可以或许带给你什么牛逼的技术,但是至少能让你明白,我们必要学习的是这些大牛的严谨的思维逻辑。
言归正传, ThreadLocalMap究竟是什么?我们要这么想,既然是线程本地变量,而且我们可以通过get和set方法可以或许获取和赋值。

[*]那我们赋值的内容,究竟保存在什么结构当中?
[*]它究竟是怎么做到线程隔离的?
[*]当我 get 和 set 的时候,它究竟是怎么做到 线程-value 的对应关系进行保存的?
ThreadLocalMap 是 ThreadLocal 非常核心的内容,是维护我们 线程 与 变量之间 关系的一个类,看到是 Map结尾,那我们也可以或许知道它现实上就是一个键值对。至于 Key 是什么,我们会在源码分析当中看出来。
Entry内部类

以下源码都是抽取讲解部门的内容来展示
static class ThreadLocalMap {    /**          * 自定义一个Entry类,并继承自弱引用      * 用来保存ThreadLocal和Value之间的对应关系      *      * 之以是用弱引用,是为了解决线程与ThreadLocal之间的强绑定关系      * 会导致假如线程没有被回收,则GC便一直无法回收这部门内容      */    static class Entry extends WeakReference> {    /** The value associated with this ThreadLocal. */    Object value;    Entry(ThreadLocal k, Object v) {      super(k);      value = v;    }}我相信有人会有疑问,假如在我要用的时候,被回收了怎么办?下面的代码会一步步地让你明白,你思量到的问题,这些大牛都已经想到并且解决了。接着往下学吧!
getEntry和getEntryAfterMiss方法

通过方法名我们就能看得出是从 ThreadLocal 对应的 ThreadLocalMap 当中获取 Entry节点,在这我们就要思考了。

[*]我们要通过什么获取对应的 Entry。
[*]我们通过上面知道使用了 弱引用,假如被 GC 回收了没有获取到怎么办?
[*]不在通过计算得到的下标上,又要怎么办?
[*]假如 ThreadLocal对应的 ThreadLocalMap不存在要怎么办?
以上这4个问题是我自己在看源码的时候可以或许想到的东西,有些问题的答案光看 ThreadLocalMap 的源码是看不出以是然的,必要结合之后的 ThreadLocal 源码分析。
/**       * 获取ThreadLocal的索引位置,通过下标索引获取内容 */private Entry getEntry(ThreadLocal key) {    // 通过hashcode确定下标    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table;    // 假如找到则直接返回    if (e != null && e.get() == key)      return e;    else      // 找不到的话接着从i位置开始向后遍历,基于线性探测法,是有可能在i之后的位置找到的      return getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {    Entry[] tab = table;    int len = tab.length;    // 循环向后遍历    while (e != null) {      // 获取节点对应的k      ThreadLocal k = e.get();      // 相等则返回      if (k == key)            return e;      // 假如为null,触发一次一连段清理      if (k == null)            expungeStaleEntry(i);      // 获取下一个下标接着进行判断      else            i = nextIndex(i, len);      e = tab;    }    return null;}一看这两个方法名,我们就知道这两个方法就是获取Entry节点的方法。
我们起首看 getEntry(ThreadLocalkey) 和 getEntryAfterMiss(ThreadLocalkey,inti,Entrye) 这个方法就看出来了,直接根据 ThreadLocal 对象来获取,以是我们可以再次证明, key 就是 ThreadLocal 对象,我们来看看它的流程:

[*]先根据 key 的 hashcode&table.length-1 来确定在 table 当中的下标。
[*]假如获取到直接返回,没获取到的话,就接着往后遍历看是否能获取到(因为用的是线性探测法,往后遍历有可能获取到效果)。
[*]进入了 getEntryAfterMiss 方法进行 线性探测,假如获取到则直接返回;获取的 key为 null,则触发一次 一连段清理(现实上在许多方法当中都会触发该方法,常常会进行一连段清理,这是 ThreadLocal 核心的清理方法),进行 垃圾回收。
expungeStaleEntry方法

这可以说是 ThreadLocal 非常核心的一个清理方法,为什么会必要清理呢?或许许多人想不明白,我们用 List 大概是 Map 也好,都没有说要清理内里的内容。
对于线程来说,隔离的 本地变量,并且使用的是 弱引用,那便有可能在 GC 的时候就被回收了。

[*]假如有许多 Entry 节点已经被回收了,但是在 table 数组中还留着位置,这时候不清理就会浪费资源
[*]在清理节点的同时,可以将后续非空的 Entry 节点重新计算下标进行排放,这样子在 get 的时候就能快速定位资源,加速效率。
/**         * 这个函数可以看做是ThreadLocal里的核心清理函数,它主要做的事情就是 * 1、从staleSlot开始,向后遍历将ThreadLocal对象被回收所在Entry节点的value和Entry节点本身设置null,方便GC,并且size自减1 * 2、并且会对非null的Entry节点进行rehash,只要不是在当前位置,就会将Entry挪到下一个为null的位置上 * 以是现实上是对从staleSlot开始做一个一连段的清理和rehash操作 */private int expungeStaleEntry(int staleSlot) {    // 新的引用指向table    Entry[] tab = table;    // 获取长度    int len = tab.length;    // expunge entry at staleSlot    // 先将传过来的下标置null    tab.value = null;    tab = null;    // table的size-1    size--;    // Rehash until we encounter null    Entry e;    int i;    // 遍历删除指定节点全部后续节点当中,ThreadLocal被回收的节点    for (i = nextIndex(staleSlot, len);         (e = tab) != null;         i = nextIndex(i, len)) {      // 获取entry当中的key      ThreadLocal k = e.get();      // 假如ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC      // 并且size-1      if (k == null) {            e.value = null;            tab = null;            size--;      } else {    // 假如不为null            // 重新计算key的下标            int h = k.threadLocalHashCode & (len - 1);            // 假如是当前位置则遍历下一个            // 不是当前位置,则重新从i开始找到下一个为null的坐标进行赋值            if (h != i) {                tab = null;                // Unlike Knuth 6.4 Algorithm R, we must scan until                // null because multiple entries could have been stale.                while (tab != null)                  h = nextIndex(h, len);                tab = e;            }      }    }    return i;}上面的代码注释我相信已经是写的很清晰了,这个方法现实上就是从 staleSlot 开始做一个一连段的清理和 rehash 操作。
set方法系列

接下来我们看看 set 方法,自然就是要将我们的变量保存进 ThreadLocal 当中,现实上就是保存到 ThreadLocalMap 当中去,在这里我们一样要思考几个问题。

[*]假如该 ThreadLocal 对应的 ThreadLocalMap 还不存在,要怎么处置处罚?
[*]假如所计算的下标,在 table 当中已经存在 Entry 节点了怎么办?
我想通过上面部门代码的讲解,对这两个问题,各人也都比较有思路了吧。
/**         * ThreadLocalMap的set方法,这个方法还是挺关键的 * 通过这个方法,我们可以看出该哈希表是用线性探测法来解决冲突的 */private void set(ThreadLocal key, Object value) {    // 新开一个引用指向table    Entry[] tab = table;    // 获取table的长度    int len = tab.length;    // 获取对应ThreadLocal在table当中的下标    int i = key.threadLocalHashCode & (len-1);    /**   * 从该下标开始循环遍历   * 1、如遇雷同key,则直接替换value   * 2、假如该key已经被回收失效,则替换该失效的key   */    for (Entry e = tab;         e != null;         e = tab) {      ThreadLocal k = e.get();      if (k == key) {            e.value = value;            return;      }      // 假如 k 为null,则替换当前失效的k所在Entry节点      if (k == null) {            replaceStaleEntry(key, value, i);            return;      }    }    // 找到空的位置,创建Entry对象并插入    tab = new Entry(key, value);    // table内元素size自增    int sz = ++size;    if (!cleanSomeSlots(i, sz) && sz >= threshold)      rehash();}private void replaceStaleEntry(ThreadLocal key, Object value,                                       int staleSlot) {    // 新开一个引用指向table    Entry[] tab = table;    // 获取table的长度    int len = tab.length;    Entry e;    // 记录当前失效的节点下标    int slotToExpunge = staleSlot;    /**   * 通过这个for循环的prevIndex(staleSlot, len)可以看出   * 这是由staleSlot下标开始向前扫描   * 查找并记录最前位置value为null的下标   */    for (int i = prevIndex(staleSlot, len);         (e = tab) != null;         i = prevIndex(i, len))      if (e.get() == null)            slotToExpunge = i;    /**   * 通过for循环nextIndex(staleSlot, len)可以看出   * 这是由staleSlot下标开始向后扫描   */    for (int i = nextIndex(staleSlot, len);         (e = tab) != null;         i = nextIndex(i, len)) {      // 获取Entry节点对应的ThreadLocal对象      ThreadLocal k = e.get();      /**         * 假如与新的key对应,直接赋值value         * 则直接替换i与staleSlot两个下标         */      if (k == key) {            e.value = value;            tab = tab;            tab = e;            // Start expunge at preceding stale entry if it exists            // 通过注释看出,i之前的节点里,没有value为null的环境            if (slotToExpunge == staleSlot)                slotToExpunge = i;            /**             * 在调用cleanSomeSlots进行启发式清理之前             * 会先调用expungeStaleEntry方法从slotToExpunge到table下标所在为null的一连段进行一次清理             * 返回值便是table[]为null的下标             * 然后以该下标--len进行一次启发式清理             * 终极内里的方法现实上还是调用了expungeStaleEntry             * 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函数             */            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);            return;      }      /**         * 假如当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点         * 则slotToExpunge赋值为当前位置         */      if (k == null && slotToExpunge == staleSlot)            slotToExpunge = i;    }    // If key not found, put new entry in stale slot    // 假如并没有在table当中找到该key,则直接在当前位置new一个Entry    tab.value = null;    tab = new Entry(key, value);    /**   * 在上面的for循环探测过程当中   * 假如发现任何无效的Entry节点,则slotToExpunge会被重新赋值   * 就会触发一连段清理和启发式清理   */    if (slotToExpunge != staleSlot)      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}/**      * 启发式地清理被回收的Entry * i对应的Entry是非无效的,有可能是失效被回收了,也有可能是null * 会有两个地方调用到这个方法 * 1、set方法,在判断是否必要resize之前,会清理并rehash一遍 * 2、替换失效的节点时候,也会进行一次清理 */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;      // Entry对象不为空,但是ThreadLocal这个key已经为null      if (e != null && e.get() == null) {            n = len;            removed = true;            /**             * 调用该方法进行回收             * 现实上不是只回收 i 这一个节点而已             * 而是对 i 开始到table所在下标为null的范围内,对那些节点都进行一次清理和rehash             */            i = expungeStaleEntry(i);      }    } while ( (n >>>= 1) != 0);    return removed;}/** * 对table进行扩容,因为要保证table的长度是2的幂,以是扩容就扩大2倍 */private void resize() {    // 获取旧table的长度,并且创建一个长度为旧长度2倍的Entry数组    Entry[] oldTab = table;    int oldLen = oldTab.length;    int newLen = oldLen * 2;    Entry[] newTab = new Entry;    // 记录插入的有效Entry节点数    int count = 0;    /**   * 从下标0开始,逐个向后遍历插入到新的table当中   * 1、如碰到key已经为null,则value设置null,方便GC回收   * 2、通过hashcode & len - 1计算下标,假如该位置已经有Entry数组,则通过线性探测向后探测插入   */    for (int j = 0; j < oldLen; ++j) {      Entry e = oldTab;      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 != null)                  h = nextIndex(h, newLen);                newTab = e;                count++;            }      }    }    // 重新设置扩容的阈值    setThreshold(newLen);    // 更新size    size = count;    // 指向新的Entry数组    table = newTab;}以上的代码就是调用 set 方法往 ThreadLocalMap 当中保存 K-V 关系的一系列代码,我就不分开再一个个讲了,这样各人看起来估计也比较方便。
我们可以来看看的 set 方法的整个实现流程:

[*]先通过 hashcode&(len-1)来定位该 ThreadLocal 在 table 当中的下标
[*]for循环向后遍历。假如获取 Entry 节点的 key 与我们必要操作的 ThreadLocal 相等,则直接替换 value。假如遍历的时候拿到了 key 为 null 的环境,则调用 replaceStaleEntry 方法进行与之替换。
[*]假如上述两个环境都是,则直接在计算的出来的下标当中 new 一个 Entry 阶段插入。
[*]进行一次 启发式地清理 并且假如插入节点后的 size 大于扩容的阈值,则调用 resize方法进行扩容。
remove方法

既然是Map情势进行存储,我们有put方法,那肯定就会有remove的时候,任何一种数据结构,肯定都得符合增编削查的。
我们直接来看看代码。
/** * Remove the entry for key. * 将ThreadLocal对象对应的Entry节点从table当中删除 */private void remove(ThreadLocal key) {    Entry[] tab = table;    int len = tab.length;    int i = key.threadLocalHashCode & (len-1);    for (Entry e = tab;         e != null;         e = tab) {      if (e.get() == key) {            // 将引用设置null,方便GC            e.clear();            // 从该位置开始进行一次一连段清理            expungeStaleEntry(i);            return;      }    }}我们可以看到,remove节点的时候,也会使用线性探测的方式,当找到对应key的时候,就会调用clear将引用指向null,并且会触发一次一连段清理。
我相信通过以上对 ThreadLocalMap的源码分析,已经让各人对其有了个基本的概念认识,相信对各人明白ThreadLocal这个概念的时候,已经不是停顿在知道它就是为了实现线程本地变量而已了。
接下来我们来看看 ThreadLocal 的源码分析。
ThreadLocal源码分析

ThreadLocal的源码相对于来说就简朴许多了,因为主要都是ThreadLocalMap这个内部类在干活,在管理我们的本地变量。
get方法系列

/**      * 获取当前线程本地变量的值 */public T get() {    // 获取当前线程    Thread t = Thread.currentThread();    // 获取当前线程对应的ThreadLocalMap    ThreadLocalMap map = getMap(t);    // 假如map不为空    if (map != null) {      // 假如当前ThreadLocal对象对应的Entry还存在      ThreadLocalMap.Entry e = map.getEntry(this);      // 并且Entry不为null,返回对应的值,否则都执行setInitialValue方法      if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;      }    }    // 假如该线程对应的ThreadLocalMap还不存在,则执行初始化方法    return setInitialValue();}ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}private T setInitialValue() {    // 获取初始值,一样平常是子类重写    T value = initialValue();    // 获取当前线程    Thread t = Thread.currentThread();    // 获取当前线程对应的ThreadLocalMap    ThreadLocalMap map = getMap(t);    // 假如map不为null    if (map != null)      // 调用ThreadLocalMap的set方法进行赋值      map.set(this, value);    // 否则创建个ThreadLocalMap进行赋值    else      createMap(t, value);    return value;}/** * 构造参数创建一个ThreadLocalMap代码 * ThreadLocal为key,我们的泛型为value */ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {    // 初始化table的大小为16    table = new Entry;    // 通过hashcode & (长度-1)的位运算,确定键值对的位置    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);    // 创建一个新节点保存在table当中    table = new Entry(firstKey, firstValue);    // 设置table内元素为1    size = 1;    // 设置扩容阈值    setThreshold(INITIAL_CAPACITY);}ThreadLocal 的 get 方法也不难,就几行代码,但是当它结合了 ThreadLocalMap 的方法后,这整个逻辑就值得我们深入研究写这个工具的人的思维了。
我们来看看它的一个流程吧:
1. 获取当前线程,根据当前线程获取对应的 ThreadLocalMap。
2. 在 ThreadLocalMap 当中获取该 ThreadLocal 对象对应的 Entry 节点,并且返回对应的值。
3. 假如获取到的 ThreadLocalMap 为 null,则证明还没有初始化,就调用 setInitialValue 方法。

[*]在调用 setInitialValue 方法的时候,会使用双重保证,再进行获取一次。
[*]ThreadLocalMap 假如依然为 null,就终极调用 ThreadLocalMap 的构造方法。
set方法系列

在这里我也不对 ThreadLocal的set方法做太多介绍了,结合上面的 ThreadLocalMap的set方法,我想就可以对上面每个方法思考出的问题有个大概的答案。
public void set(T value) {    // 获取当前线程    Thread t = Thread.currentThread();    // 获取线程所对应的ThreadLocalMap,从这可以看出每个线程都是独立的    ThreadLocalMap map = getMap(t);    // 假如map不为空,则k-v赋值,看出k是this,也就是当前ThreaLocal对象    if (map != null)      map.set(this, value);    // 假如获取的map为空,则创建一个并保存k-v关系    else      createMap(t, value);}其实 ThreadLocal的set方法很简朴的,最主要的都是调用了 ThreadLocalMap的set方法,内里才是真正核心的执行流程。
不过我们照样来看看这个流程:

[*]获取当前线程,根据当前线程获取对应的 ThreadLocalMap
[*]假如对应的 ThreadLocalMap不为null,则调用其的set方法保存对应关系
[*]假如map为null,就终极调用 ThreadLocalMap的构造方法创建一个 ThreadLocalMap并保存对应关系
总结

上面通过对 ThreadLocal 和 ThreadLocalMap 两个类的源码进行了分析,我想对于ThreadLocal 这个功能的一整个流程,各人都有了个比较清晰的了解了。
以是通过该篇源码的分析,让我真正地意识到,我们不能光是看源码做翻译而已,我们一定要学会他们是如何思考实现这么个功能,我们要学会他们思考每一个功能的逻辑。
页: [1]
查看完整版本: 详解ThreadLocal和ThreadLocalMap源码