【Java 集合】ThreadLocal

2023-12-25 22:30:22

1 简介

在多线程编程中,我们经常面临共享数据的问题,而这可能引发一系列并发性和线程安全性的挑战。
Java 提供了许多机制来处理这些问题,比如控制并发的各种锁, 控制线程串行地修改资源, 避免线程安全, 或者通过关键字 volatile 修饰变量, 保证可见性等。
而在解决并发共享的方式中, 还有一种方式, 那么就是线程隔离, 每个线程各自维护资源的副本, 从而避免了共享资源的竞争。
而实现这个实现的一个经典代表就是 ThreadLocal, ThreadLocal 为每个线程提供了独立的变量副本, 从而在不同线程之间隔离数据。这为我们提供了一种简单而强大的方式来确保线程间数据的安全性。

ThreadLocal 的特点:

  1. 线程隔离性: 每个线程都有自己独立的 ThreadLocal 变量, 彼此之间不共享数据
  2. 数据共享: ThreadLocal 变量在同一个线程内共享,这意味着在同一线程中的不同方法可以轻松地访问相同的 ThreadLocal 变量
  3. 线程安全性: 单个 ThreadLocal 实例通常是线程安全的,因为每个线程操作的是自己的副本,而不是共享的实例
  4. 内存泄漏风险: 因为一个变量, 每个线程都会以副本的形式存储在自己身上, 这本身就是一种消耗空间的行为, 同时它的生命周期和 Thread 一样长, 在使用过程中不手动进行删除, 可能会导致内存泄露

2 ThreadLocal 的实现原理

public class Demo {

    public static void main(String[] args) {

        ThreadLocal<Integer> numThreadLocal = new ThreadLocal<>();
        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

        numThreadLocal.set(1);

        stringThreadLocal.set("hello");

        // 1
        System.out.println(numThreadLocal.get());
        // 2
        System.out.println(stringThreadLocal.get());
    }
}

假设现在我们执行上面代码的的线程名为 Thread-1。

从代码上面直观看到的效果如下:
Alt 'ThreadLocal 表面的效果'

程序启动后, 创建了 2 个 ThreadLocal

  1. numThreadLocal: 存储 Integer 类型的数据
  2. stringThreadLocal: 存储 String 类型的数据

然后 Thread-1 将 1 存入 numThreadLocal, 将 “hello” 存入 stringThreadLocal
最后 Thread-1 通过 get 方法分别从 numThreadLocal 和 stringThreadLocal 中获取到了 1 和 “hello”。

看起来的我们的数据都是存储在 ThreadLocal 中的。

但是实际中, ThreadLocal 在整个过程中发挥的作用不是存储数据, 而是一个转换器的效果, 真正的数据还是存储在 Thread 中的, 存储的地方是一个 ThreadLocalMap, 可以理解为一个 Map。
每创建一个 ThreadLocal 都分配给自己的唯一编码, 调用这个 ThreadLocal 的 get, set 等方法, ThreadLocal 都是通过这个 code 定位到调用的线程的内部的一个 ThreadLocalMap 的某个位置, 然后给这个 Map 添加或删除数据。
不同的 ThreadLocal 有不同的 code, 所以通过这个 code 定位到 Thread 的 ThreadLocalMap 的某个位置, 从而实现了线程隔离 (实际这个通过 code 定位到具体的位置涉及到 hash, 所以还是有冲突的可能, 但是通过别的方式解决了)。

Alt 'ThreadLocal 实际的效果'

我们先通过它的几个核心方法简单了解一下。

2.1 设值到 ThreadLocal - set() 方法

public class ThreadLocal<T> {

    public void set(T value) {

        // 获取当前的线程
        Thread t = Thread.currentThread();
        // 通过当前线程实例获取到 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        
        if (map != null)
            // 如果 map 不为 null, 则以当前 threadLocal 实例为 key, 值为 value 进行存入
            // ThreadLocalMap 内部的 set 会通过 this, 得到当前的 ThreadLocal, 然后获取里面的 code
            map.set(this, value);
        else
            // map 为 null, 则新建 ThreadLocalMap 并存入 value
            createMap(t, value);
    }

    /**
     * 返回指定线程自身的 ThreadLocalMap 对象
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * 为指定的线程创建一个 ThreadLocalMap 对象
     */
    void createMap(Thread t, T firstValue) {
        // 这里同 ThreadLocalMap 的 set, 会通过 this, 得到当前的 ThreadLocal, 然后获取里面的 code
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

}

从上面的代码, 我们可以知道
ThreadLocal 实际的数据是存在每个线程自身的 ThreadLocalMap 对象中。

那么 ThreadLocalMap 是什么样的呢?

首先 ThreadLocalMap 是定义在 Thread 身上的一个属性。

public class Thread {
    // 每个线程内部都有一个 ThreadLocalMap 属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

我们可以在类 A 中声明一个 TheadLocal, 然后在类 B 中用声明一个 TheadLocal
同一个线程同时使用到了类 A 和类 B, 那么线程身上的 TheadLocalMap threadLocals 需要支持同时存储 2 个数据以上, 那么 ThreadLocalMap
应该是什么样的一个数据结构呢?

答案是数组, 最终实现出来的结果类似于 HashMap, value 是我们存入的内容, 而 key 就是每个类上的 ThreadLocal 的实例内部的一个 code。

通过这个 TheadLocal 的 code 得到一个 hash 值, 通过这个 hash 值计算得到其在数组中的哪个位置。
这时候如果有另外一个 TheadLocal 实例, 他的 code 不一样, 计算出来的 hash 不一样, 那么这个 TheadLocal 的内容存在数组的另外一个位置。

TheadLocalMap 的理解可以简单的先按照上面的方式, 后面会具体的分析。

向 ThreadLocal 放数据, 也就是 set 过程

  1. 获取当前线程的所维护的 threadLocalMap
  2. 若 threadLocalMap 不为 null, 则以当前的 ThreadLocal 实例内部的 code 为 key, 值为 value 的键值对存入 threadLocalMap
  3. 若 threadLocalMap 为 null 的话, 就为当前的线程新建一个 ThreadLocalMap, 然后在以当前的 ThreadLocal 实例内部的 code 为 key, 值为 value 的键值对存入即可

2.2 从 ThreadLocal 中取值 - get() 方法

public class ThreadLocal<T> {

    public T get() {

        // 1. 获取当前的线程
        Thread t = Thread.currentThread();

        // 2. 获取当前线程的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);

        // 3. 当前线程的 ThreadLocalMap 不为空
        if (map != null) {

            // 获取 map 中 key 为当前 ThreadLocal 内部 code 的 Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
        
            // 获取到了需要的 Entry
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }

        //若 map 为 null 或者 entry 为 null 的话通过该方法初始化, 并返回该方法返回的 value
        return setInitialValue();
    }

    /**
     * 为当前的线程设置一个 value 为 null 的 ThreadLocalMap 
     */ 
    private T setInitialValue() {

        // 获取一个 null 的 value
        T value = initialValue();

        Thread t = Thread.currentThread();

        // 获取当前线程的 TheadLocalMap
        ThreadLocalMap map = getMap(t);
        // 不为 null, 设值
        if (map != null)
            map.set(this, value);
        else
            // 为 null, 创建一个
            createMap(t, value);
        return value;
    }

    /**
     * 默认返回一个 null
     */
    protected T initialValue() {
        return null;
    }

}

从 ThreadLocal 取数据, 也就是 get 的过程

  1. 获取当前线程的所维护的 threadLocalMap
  2. 若 threadLocalMap 不为 null, 通过当前 ThreadLocal 的 code 为 key 从 threadLocalMap 中获取存储数据的 Entry, Entry 不为 null, 就是找到需要的值了, 返回
  3. threadLocalMap 为 null, 或者从 threadLocalMap 获取到的 Entry 为空, 声明一个初始的 value 默认为 null, 走一遍 set 的逻辑
  4. 返回声明的初始 value, 也就是 null

2.3 从 ThreadLocal 中删除数据 - remove() 方法

public class ThreadLocal<T> {

    public void remove() {
        // 获取当前线程的 TheadLocalMap 
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            // 从 map 中删除以当前 threadLocal 实例内部 code 为 key 的键值对
            // ThreadLocalMap 的 remove 后面在讲
            m.remove(this);
    }
}

remove 的方法很简单的, 就是从从当前线程获取到 ThreadLocalMap, 不为 null 的话, 调用 ThreadLocalMap 的 remove 进行移除数据。

3 ThreadLocalMap 的实现原理

从上面的分析我们已经知道, 数据其实都放在了线程自身的 ThreadLocalMap 中, ThreadLocal 的 get, set 和 remove 方法实际上是通过 ThreadLocalMap
的 getEntry, set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal, 就是在了解 TheadLocalMap。

3.1 ThreadLocalMap 中的 Entry

ThreadLocalMap 是 ThreadLocal 一个静态内部类, 和大多数容器一样内部维护了一个数组, 同样的 ThreadLocalMap 内部维护了一个 Entry 类型的 table 数组

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

    Object value;

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

Entry 是一个以 ThreadLocal 内部 code 为 key, value 为 Object类型 的键值对。 有点类似于 HashMap, T key, Object value。 key 的变量声明在父级 WeakReference 中。
Entry 继承了 WeakReference, 同时在构造函数中将当前的 ThreadLocal 作为参数传递给了父级, 在父级中, 会将对应的 TheadLocal 封装为一个弱引用。
而不像我们平时直接在 Entry 中多声明一个 T key, 然后将 TheadLocal<?> k 赋值给 key, 这种做法是强引用, key 和当前的 Entry 是共生的, 一起存在一次消亡。
而使用弱引用进行包装可以达到: 在 GC 时, 在 Entry 存在的情况下, k 能被回收。

Java 中的 4 种引用可以看下面的附录, 有简单的介绍。

Thead, TheadLocal, TheadLocalMap 三者的关系如下:
Alt 'Thread 和 ThreadLocal 和 ThreadLocalMap 三者之间的关系'

实线表示强引用, 虚线表示弱引用。

每个线程实例中可以通过 Thead.threadLocals 获取到 ThreadLocalMap, 而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例内部 code 为 key, 任意对象为 value 的 Entry 数组。

当我们为 ThreadLocal 变量赋值时, 就是给对应的线程的 ThreadLocalMap 中放入一个当前 ThreadLocal 实例内部 code 为 key, 值为 value 的 Entry。

需要注意的是 Entry 中的 key 是弱引用, 当 ThreadLocal 外部强引用被置为 null (ThreadLocalInstance = null) 时, 那么系统 GC 的时候,
根据可达性分析, 这个 ThreadLocal 实例就没有任何一条链路能够引用到它, 这个 ThreadLocal 势必会被回收。 这样一来, ThreadLocalMap 中就会出现
key 为 null 的 Entry, 就没有办法访问这些 key 为 null 的 Entry 的 value。如果当前线程再迟迟不结束的话, 这些 key 为 null 的 Entry 的
value 就会一直存在一条强引用链: Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value 永远无法回收, 造成内存泄漏。
当然, 如果当前 Thread 运行结束, ThreadLocal, ThreadLocalMap, Entry 没有引用链可达, 在垃圾回收的时候都会被系统进行回收。
在实际开发中, 会使用线程池去维护线程的创建和复用, 比如固定大小的线程池, 线程为了复用是不会主动结束的。所以, ThreadLocal 的内存泄漏问题, 是应该值得我们思考和注意的问题。

3.2 ThreadLocalMap 的创建

创建方式一

static class ThreadLocalMap {

    // 存放数据的数组
    private Entry[] table;

    // 数组当前的元素个数
    private int size = 0;

    // 数组中的元素个数达到了这个就会扩容, 而不是数组满了才扩容
    private int threshold;

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {

        // INITIAL_CAPACITY = 16;
        table = new Entry[INITIAL_CAPACITY];

        // 调用 ThreadLocal 的 threadLocalHashCode 的 threadLocalHashCode 属性值, 然后 & (16 - 1) 
        // 得到这个 firstKey 应该存在数组的哪个位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

        // 在数组对应的位置存入 Entry
        table[i] = new Entry(firstKey, firstValue);
        // 当前容量设置为 1 
        size = 1;
        // setTheshold 里面只有一行代码 threshold = len * 2 / 3;
        // 也就是设置阈值 = 长度 * 2/3
        setThreshold(INITIAL_CAPACITY);
    }

}

创建方式二

static class ThreadLocalMap {

    private ThreadLocalMap(ThreadLocalMap parentMap) {

        // 取到入参 ThreadLocalMap 的数组
        Entry[] parentTable = parentMap.table;

        // 入参 ThreadLocalMap 的数组的长度, 这里的长度不用做任何处理, 因为这里的长度必定为 2 的 n 次方
        int len = parentTable.length;

        // 设置阈值 = len * 2/3
        setThreshold(len);

        // 设置新的数组    
        table = new Entry[len];

        // 遍历入参的 ThreadLocalMap 的数组
        for (int j = 0; j < len; j++) {
            // 获取到 table 数组的第 j 个位置
            Entry e = parentTable[j];
            if (e != null) {
                // 对应的位置有数据
                
                // 从 e 中获取对应的 key  即 ThreadLocal 实例
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();

                if (key != null) {
                    // 调用 ThreadLocal 的 childValue 方法
                    // 在 ThreadLocal 中这个方法的默认实现为 throw new UnsupportedOperationException(); 抛出一个异常
                    // 也就是这里的 key 必须是 TheadLocal 的子类, 同时重写了 childValue 方法, 否则会报错
                    Object value = key.childValue(e.value);

                    // 把 key 和 value 封装为 Entry
                    Entry c = new Entry(key, value);
                    // 定位到放在数组的位置
                    int h = key.threadLocalHashCode & (len - 1);
                    // 对应的位置不为空, 查询后面的一个位置, 到了尾部, 回到头部继续往后找
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    // 对应的位置 设置为当前的 Entry
                    table[h] = c;
                    // 个数加 1
                    size++;
                }
            }
        }
    }

    /**
     * 位置的定位
     */
    private static int nextIndex(int i, int len) {
        // 当前的位置 + 1 小于当前的长度, 取 + 1 的位置, 否则取 0, 既头部的位置
        return ((i + 1 < len) ? i + 1 : 0);
    }

}

从上面的方法中可以看出一个有趣的点:

  1. HashMap 在处理 hash 碰撞时使用的是: 拉链法
  2. ThreadLocalMap 在处理 hash 碰撞时使用的是: 开放定址法 (一旦产生了冲突, 就按某种规则去寻找另一空地址) 中的线性探测法

具体的开放地址法如何解决 hash 冲突的可以看一下这篇文章: 冲突处理方法----开放定址法

3.3 ThreadLocalMap 中决定 Entry 的 hashCode 的计算

在 ThreadLocalMap 中 key 对应的 Entry 在 Entry[] table 的位置的计算方式为 key.threadLocalHashCode & (len - 1), 这里的 key 就是我们传入的 ThreadLocal。
决定位置的元素有 2 个

  1. 当前 Entry[] table 数组的长度
  2. key 对应的 ThreadLocal 中的 threadLocalHashCode 的值 (这个 code 就是上面我们一直说的 ThreadLocal 内部的 code)

Entry[] table 的长度很容易知道, 但是 threadLocalHashCode 是如何计算的呢

public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    // 这个值为 1640531527
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

先说结论:

每个 ThreadLocal 实例的 threadLocalHashCode 在 ThreadLocal 实例创建出来后就不会再变了, 这个 ThreadLocal 实例后面在使用中, threadLocalHashCode 固定的。
每个 ThreadLocal 实例的 threadLocalHashCode 都是不同的。

  1. nextHashCode 这个变量在 ThreadLocal 中被 static 修饰了, 说明所有的 ThreadLocal 的实例共用 1 个 AtomicInteger 的属性
  2. 当我们声明第 1 个 ThreadLocal, 调用唯一的 nextHashCode, 得到了当前的 threadLocalHashCode 值, 值为 0, 同时让唯一的 AtomicInteger 的值增加 1640531527
  3. 声明第 2 个 ThreadLocal, 又调用了唯一的 nextHashCode, 得到了当前的 threadLocalHashCode 值, 值为 1640531527, 然后又让 AtomicInteger 的值增加 1640531527
  4. 这样每创建一个 ThreadLocal 的 nextHashCode 都是不一样的。不要忽略了 int 值的取值范围, 导致值的变化
  5. 为什么每个 ThreadLocal 之间的的 nextHashCode 的差值为 1640531527 ? 这个和斐波那契散列有关, 它可以使 hashcode 均匀的分布在大小 2 的 N 次方的数组里。至于为什么, 涉及到数学相关的知识, pass !

通过上面的分析基本知道了 ThreadLocal 的大体实现了吧

  1. 每次声明了一个 ThreadLocal 实例, 这时候这个实例会得到一个固定的 hashCode, 存储在 threadLocalHashCode 变量中
  2. 每个线程中都有一个变量 threadLocals, 类型为 ThreadLocalMap
  3. ThreadLocalMap 本质是一个 Entry 的数组, Entry 对象中存储了 key, value, key 默认一个 ThreadLocal 的实例内部的 hash code, value 就是通过 ThreadLocal 存储到 Thread 的内容
  4. 当我们调用 ThreadLocal 的实例, 将内容存储到当前的 Thread 本质是存在 Thread 中的 threadLocals 属性, 也就是 Entry[] 数组中

调用 ThreadLocal 的实例, 存放数据的步骤大体如下

  1. 创建一个 ThreadLocal 的实例, 实例的得到一个接近唯一的 hashCode, 存储在自身的 threadLocalHashCode 变量
  2. 获取当前的线程维护的 threadLocals 变量, 数据类型为 ThreadLocalMap, 本质就是一个 Entry[] 的数组, 如果为空, 进行初始化
  3. 将当前的 ThreadLocal 实例内部的 threadLocalHashCode 作为 key, 需要存储的内容做为 value, 封装为一个 Entry 实例
  4. 通过当前的 ThreadLocal 实例的 threadLocalHashCode 和当前 Entry[] 数组的长度计算出, 这个 ThreadLocal 存储在数组中的哪一个位置, 把封装好的 Entry 实例放到数组中
  5. 这时候又声明了一个新的 ThreadLocal 的实例, 又是另一个 threadLocalHashCode
  6. 往这个新的 ThreadLocal 的实例存值, 同样是封装为 Entry,
  7. 当前线程的 threadLocals 变量不为空, 不会初始化, 也就是新的 ThreadLocal 和旧的 ThreadLocal 用着同一个 threadLocals 变量, 也就是同一个 Entry[] 数组
  8. 但是新的 ThreadLocal 的实例的 threadLocalHashCode 不一样, 计算出来的位置也会不一样, 这样放到同一个 Entry[] 数组的不同位置
  9. 也就是说使用 ThreadLocal 存储数据时, 数据是存储在当前线程的数组中的

3.4 ThreadLocalMap 的 set() 方法

static class ThreadLocalMap {
    
    private void set(ThreadLocal<?> key, Object value) {

        // 获取到当前存数据的数组引用
        Entry[] tab = table;
        // 数组长度
        int len = tab.length;
    
        // 当前 ThreadLocal 的 threadLocalHasCode & (数组的长度 - 1), 得到当前应该存在数组的哪个位置
        int i = key.threadLocalHashCode & (len-1);

        // 取到对应位置的元素,  但 i 位置的 不为 null
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

            // 这时候的 e 一定不会是 null

            // 从 Entry e 中获取 key
            ThreadLocal<?> k = e.get();

            // 对应的 k == 输入的 key, 替换新值, 结束
            if (k == key) {
                e.value = value;
                return;
            }

            // 对应的位置的 k 为 null, 将当前的 value 设置到 ThreadLocalMap 的数组
            // Entry 不为 null, 但是 Entry 的 key 为 null, 情况是存在的, 因为 Entry 中的 key 是一个弱引用, 能被 GC 直接回收
            // 这时候可以把当前的 Entry 放到这个位置
            if (k == null) {

                // 将当前的 value 放到数组 i 的位置, 进行 table 数组的重新整理, 即对数组中, 无效的 Entity (key 为 null) 进行移除
                // 然后对数组中的 Entry 通过其 key 的 hashCode 计算出来的位置和这个 Entry 当前所在的位置不一致 (hash 冲突过), 进行重试计算位置
                // 这样经过了无效 Entry 删除后, 重新计算位置, 可能将其重新放到正确的位置
                replaceStaleEntry(key, value, i);
                return;
            }

            // 到了这里, 会执行 for 后面的 e = tab[i = nextIndex(i, len)], e != null, 又有进入到这个循环
            // 等于 null 的话, 跳出循环, 执行下面的逻辑
        }

        // i 位置为 null, 创建一个新的 Entry 放到数组的 i 位置
        tab[i] = new Entry(key, value);
        // 容量加 1
        int sz = ++size;

        // cleanSomeSlots 没有清除过无效的 Entry, 同时当前数组中的数据已经大于阈值了, 进行扩容
        // 插入后再次清除一些 key 为 null 的 "脏" entry, 如果大于阈值还是需要扩容 
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            // 扩容
            rehash();
    }
}
24.3.4.1 replaceStaleEntry() 方法: 无效 Entry 清理和 Entry 位置重计算

static class ThreadLocalMap {

    /**
     * 移除数组中的部分无效 Entry, 然后对范围内的 Entry 重新地位 
     * 
     * @param key       ThreadLocal 实例 的 key
     * @param value     需要存储的值
     * @param staleSlot key 当前打算放到这个位置, 同时这个位置的 Entry 的 key 为 null, 为当前 key 的最优位置
     */
    private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {

        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        // 下面的所有的遍历, 基本都是
        // 1. 向前遍历, 遍历到数组的头部后, 会继续从数组的尾部开始继续往前遍历
        // 2. 向后遍历, 遍历到数组的尾部后, 会继续从数组的头部开始继续向后遍历
        // 所以把存储数据的 table 数组看成一个头尾相接的环, 可以容易理解

        // staleSlot 位置的 Entry key 为 null
        // 在向前遍历中, 遇到的 Entry key 问 null, slotToExpunge 就会变更为这个 Entry 所在的数组位置
        int slotToExpunge = staleSlot;

        // 从当前的 staleSlot 位置向前(如果到了头部, 则从尾部继续往前找), 直到遇到第一个位置 Entry 为空才停止
        
        // prevIndex 的逻辑就一句话: ((i - 1 >= 0) ? i - 1 : len - 1); 
        // 位置 i - 1 >=0, 没到数组的头部, 向前一位,
        // 位置 i - 1 <0, i 已经是数组的头部了, 那么从 len - 1, len 为数组长度, len - 1 就是数组的尾部

        // 往前找的过程中, slotToExpunge 会不断重置, 当前位置的 Entry 的 key 为 null, slotToExpunge 更新为这个 Entry 所在的位置

        // 这个循环直到 (e = tab[i]) == null 才结束, 就是对应的位置为空, 没有数据
        for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
            // 对应对象的 key 为 null, 变更 slotToExpunge 等于当前的数组索引位置 i
            if (e.get() == null)
                slotToExpunge = i;


        // 从 staleSlot 开始向后找(如果到了尾部了, 从头部继续找), 直到遇到第一个位置没有数据的才结束

        // 从当前的 staleSlot 位置向后找(如果到了尾部了, 则从头部继续找后找), 直到下面 2 种情况才停止
        // 1. 直到遇到第一个位置 Entry 为空才停止, 把当前的 Entry 放到 staleSlot 的位置
        // 2. 在遍历中找到了 Entry 的 key 和当前要存储的 Entry 的 key 一样  => 将当前的 value 放到 找到的 Entry, 同时找到的 Entry 和 staleSlot 的 Entry 交换

        // nextIndex 的逻辑就一句话: ((i + 1 < len) ? i + 1 : 0);
        // 位置 i + 1 < len, 没到数组的尾部部, 向后一位,
        // 位置 i + 1 >= len, i 已经是数组的尾部了, 从数组的第 0 位, 也就是头部开始

        // 这个循环直到 (e = tab[i]) == null 才结束, 就是对应的位置为空, 没有数据
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {        

            // 在这里面的 e 不为 null

            // 获取 entity 的 key, 也就是 ThreadLocal
            ThreadLocal<?> k = e.get();

            // 当前的 Entry 的 key 等于我们要存储的数据的 key 
            if (k == key) {
                // 更新当前的 Entity 的 value 为我们要存储的 value
                e.value = value;

                // 上面的入参说过了, staleSlot 是当前需要存储的 ThreadLocal key 计算后存储的最优位置, 同时这个位置的 Entry 的 key 为 null
                // 所以将 staleSolt 和当前找到的 key 一样的 Entry 进行交换

                // 将 i 和一开始无效的位置 staleSlot 替换
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // 如果上面的向前查找过程中没有更新 slotToExpunge 的值, 则更新 slotToExpunge 为当前值 i, staleSlot 的值是存在的
                // slotToExpunge 初始值 = staleSlot, 在上面的向前找 Entry 中, 如果 staleSlot 向前的到第一个为空的距离间, 所有的 Entry 的 key 都不为 null
                // slotTOExpunge 没有更新, 也就是还是等于 staleSlot 

                if (slotToExpunge == staleSlot)
                    // 从 nextIndex, 可以得知 i 是在 staleSlot 的后面的, 不等于 staleSlot
                    // 那么把 i 赋值诶 slotToExpunge, 此时的 i 是 Entry key 为空的元素
                    slotToExpunge = i;
                
                // expungeStaleEntry 将从 slotToExpunge 到向后遇到的第一个为空的位置(环形的遍历, 可以从尾部回到头部), 这段距离
                // 1. 无效的 Entry, 既 Entry 的 key 为 null 的清空
                // 2. 有效的 Entry, 拿到 key 的 hashCode 重新计算自身的最优位置, 最优位置和当前 Entry 所在的位置不一样, 重试重新调整这个 Entry 的位置
                // 同时返回遇到的第一个为空的位置的位置索引

                // cleanSomeSlots 从第一个入参的位置开始, 向后遍历 log2(第二个入参) 次, 同样是环形遍历,
                // 同样是 对 Entry 的 key 为 null 的进行清除, 对有效 Entity 的位置不等于直接计算出来的, 重新分配位置
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;     
            }

            // key 为 null 并且当前的 slotToExpunge 还是等于 staleSlot(也就是上面向前找的时候, 没有找到符合条件的), 
            // 更新 slotToExpunge 为当前的 i(这时候的 entity 为无效 entity)
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
            }  
        }

        // 把 staleSlot 位置设置为当前的 key 和 value 组合的 Entity
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 如果 slotToExpunge 和 staleSlot 不行等, 表示有数据可以清除, 调用 expungeStaleEntry 和 cleanSomeSlots 进行清除
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
    }

    /*
     * 从入参的位置开始, 向后遍历到第一个位置为空的位置 (环形遍历), 
     * 将这段距离中的无效 Entity 移除, 有效 Entity 的位置不等于直接计算出来的, 重新分配位置
     * @param staleSlot 开始移除的位置
     * @return 遇到的第一个为 null 的数组位置
     */
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 先直接将 staleSlot 位置的置为 null
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        Entry e;
        int i;

        // nextIndex 和 后面的 (e = tail[i]) != null, 老套路
        // 从 staleSolt 向后遍历, 直到遇到第一个为 null 停止, 注意是环形的, 会从尾部回到头部
        for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {

            ThreadLocal<?> k = e.get();
            if (k == null) {
                // 当前位置的 Entry 的 key 为 null, 清掉
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 当前位置的 Entry 的 key 不为 null
                // 获取这个 Entry 的 key 直接计算后的直接存储位置
                int h = k.threadLocalHashCode & (len - 1);

                // 当前存储的位置和直接计算出来的位置不一样, 重新试着调整一下位置
                if (h != i) {
                    // 把当前的位置置为 null, 方便下面的循环, 最坏的情况能到这个位置停下来, 即重新计算后的位置和当前的一样, 是最优的位置了
                    tab[i] = null;

                    // 从计算出来的位置 h 向后遍历到第一个为 null 的位置 (nextIndex 环形的)
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    // 把计算出来的新位置设置为当前的 Entry    
                    tab[h] = e;
                }
            }
        }
        return i;
    }

    /**
     * 从 i 位置开始, 向后遍历 log2(n) 次, 同样是环形遍历,
     * 在这 log2(n) 次遍历中, 对 Entry 的 key 为 null 的进行清除, 对有效 Entity 的位置不等于直接计算出来的, 重新分配位置
     * 对无效的 entity 进移除和对算出来的位置不一致的重新计算
     *
     * 控制遍历的次数在 log2(n) 次数, 是一个时间上的折中控制, 全数组扫描会和耗时, 扫描次数太少又没太大意义
     *
     * @param i 数组中为空的位置
     * @param n 需要扫描的次数, 一般都是当前数组的长度
     */
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            // i 的下一个位置 (环形的)
            i = nextIndex(i, len);

            Entry e = tab[i];
            // 下一个位置不为空, 但是 key 为空
            if (e != null && e.get() == null) {
                // n 更新为当前数组的禅道
                n = len;
                // 有数据被清空
                removed = true;

                // 调用到上面的 expungeStaleEntry 方法
                // 对从 i 向后遍历遇到的一个位置为空的距离(环形遍历), 清空无效的 Entry, 将有效 Entry 直接计算出来的位置和当前位置不一样的, 进行位置的重新调整
                // 返回遇到的第一个位置为空的位置索引
                i = expungeStaleEntry(i);
            }
        // n>>> 1 等于 n / 2    
        } while ( (n >>>= 1) != 0);

        // 返回这次清洗中是否有无效 Entry 被清除过
        return removed;
    }
}
24.3.4.2 rehash() 方法: 存储数据的数组扩容
static class ThreadLocalMap {

    private void rehash() {
        
        // 对这个数组中的无效 Entry 调用 expungeStaleEntry(无效 Entry 的位置) 方法进行清理
        expungeStaleEntries();

        // 通过 expungeStaleEntries 尝试清理后
        // 当前的数组容量还是大于 阈值的 3/4, 真正的扩容
        if (size >= threshold - threshold / 4)
            resize();
    }
    
    /**
     * 对整个数组中的无效 Entry 所在的位置调用 expungeStaleEntry() 进行无效 Entry 的清除和有效 Entry 的重新分配位置
     */
    private void expungeStaleEntries() {

        Entry[] tab = table;
        int len = tab.length;
        // 遍历整个数组, 通过 expungeStaleEntry 进行数组数据的清除和位置重置
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            // 对数组中 Entry 不为 null, 但是 key 为 null 的位置
            // 调用 expungeStaleEntry() 方法对当前数组从这个位置开始, 向后遍历到第一个为位置为空的位置 (环形遍历), 对这段距离内
            // 将这段距离中的无效 Entity 移除, 有效 Entity 的位置不等于直接计算出来的, 重新分配位置
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }     


    private void resize() {

        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        // 长度扩大为原来的 2 倍
        int newLen = oldLen * 2;

        Entry[] newTab = new Entry[newLen];
        int count = 0;

        // 遍历旧数组
        for (Entry e : oldTab) {

            // 不为空
            if (e != null) {
                ThreadLocal<?> k = e.get();
                // 无效的 entity 
                if (k == null) {
                    // 值也置为空, 帮助 GC
                    e.value = null;
                } else {
                    // 计算出新的位置
                    int h = k.threadLocalHashCode & (newLen - 1);
                    // 冲突检测, 如果对应的位置有值了, 则找下一个位置
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);  
                    newTab[h] = e;
                    // 个数加 1 
                    count++;
                }
            }
        }

        // 更新阈值为当前长度的 2/3
        setThreshold(newLen);
        size = count;
        table = newTab;
    }
}

上面就是这个 ThreadLocalMap 的 set 方法的整体流程, 繁杂的一个大流程, 整理后, 我们可以知道

1. 怎样确定新值插入到哈希表中的位置

每个 ThreadLocal 的 threadLocalHashCode 都是在 ThreadLocal 声明的时候就确定了, 通过 ThreadLocal 实例的 threadLocalHashCode & (当前数组的长度 - 1) 就能得到当前的 value 放在数组的位置

2. 当存放的位置出现冲突了, 怎么解决冲突的

源码中通过 nextIndex(i, len) 方法解决 hash 冲突的问题, 该方法为 ((i + 1 < len) ? i + 1 : 0);
也就是不断往后线性探测, 当到哈希表末尾的时候再从 0 开始, 成环形, 直到找到数据为空的位置, 这个就是这个 Entry 存放的地方了

3. 怎样解决无效 Entry
在 set() 方法的 for 循环中寻找和当前 Key 相同的可覆盖 Entry 的过程中通过 replaceStaleEntry 方法解决脏 entry 的问题。
如果当前 table[i] 为 null 的话, 直接插入新 Entry 后也会通过 cleanSomeSlots() 来解决脏 entry 的问题

4. 扩容的时机和方式
扩容的时机:
表面上是当前存放数据的个数 = 数组的长度 * 3/4;
而实际是 当前存放数据的个数 = 数组的长度 * 3/4 时, 会触发一次全数组的无效 Entry 清除和有效 Entry 的位置重算。
经过清除和重算后, 当前存放数据的个数还是 >= 当前阈值的 *3/4 (阈值 = 数组的长度 * 2/3) = 当前数组的 1/2 就扩容了

扩容的方式: 当前的长度变为 2 倍, 然后把旧的数据移到新的数组中

3.5 ThreadLocalMap 的 getEntry() 方法

static class ThreadLocalMap {

    private Entry getEntry(ThreadLocal<?> key) {

        // 计算出位置
        int i = key.threadLocalHashCode & (table.length - 1);

        Entry e = table[i];

        // e 不为空, 同时对应的 key 一致, 也就是直接找到了, 就返回这个 entity
        if (e != null && e.get() == key)
            return e;
        else
            // 找到的位置的 Entry 为空 或者 Entry 的 key 不等于当前的 key
            return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {

        Entry[] tab = table;
        int len = tab.length;

        // 变量 Entry 不为空, 也就是当前的 key 直接计算出来的位置有数据, 才能进入到下面的循环, 否则直接返回 null
        while (e != null) {

            ThreadLocal<?> k = e.get();
            // 再次判断获取到的 Entry 的 key 是否等于当前的 key
            if (k == key)
                // 一样直接返回入参的 Entry
                return e;

            // k 为 null     
            if (k == null)
                // 无效 Entry 回收, 有效 Entry 位置重置
                expungeStaleEntry(i);  
            else
                // 下一个位置
                i = nextIndex(i, len);  
            // 获取新位置的 Entry       
            e = tab[i];
        }

        // 返回 null
        return null;

    }
}

3.6 ThreadLocalMap 的 remove() 方法

static class ThreadLocalMap {

    private void remove(ThreadLocal<?> key) {

        Entry[] tab = table;
        int len = tab.length;
        // 计算出位置
        int i = key.threadLocalHashCode & (len-1);

        // 遍历 i 和其后面的 Entity, 直到 key 找到了或者遇到了空
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                // 无效 Entry 清除, 有效 Entry 位置重算
                expungeStaleEntry(i);
                return;
            }
        }
    }
}

4 ThreadLocal 的使用场景

ThreadLocal 不是用来解决共享对象的多线程访问问题的, 数据实质上是放在每个 Thread 实例引用的 ThreadLocalMap, 也就是说每个不同的线程都拥有专属于自己的数据容器 (ThreadLocalMap), 彼此不影响。

因此 ThreadLocal 只适用于 共享对象会造成线程安全 的业务场景。
比如 Hibernate 中通过 HhreadLocal 管理 Session 就是一个典型的案例, 不同的请求线程 (用户) 拥有自己的 session, 若将 session 共享出去被多线程访问, 必然会带来线程安全问题。

5 附录

5.1 引用

在 Java 中, 我们声明了一个对象, 这个对象一般情况下是在堆中, 而创建的线程只是持有对象的引用, 通过这个引用调用创建的对象。

而对于这个对象的内存分配和内存回收, 都不需要程序员负责, 都是由 JVM 去负责。一个对象是否可以被回收, 主要看是否有引用指向此对象, 说的专业点, 叫可达性分析。
所以,引用对应 JVM 有着重要的作用。

Java 设计这四种引用的主要目的有两个

  1. 可以让程序员通过代码的方式来决定某个对象的生命周期
  2. 有利用垃圾回收

5.2 Java 的四种引用

1. 强引用
只要某个对象有强引用与之关联, 这个对象永远不会被回收, 即使内存不足, JVM 宁愿抛出 OOM, 也不会去回收

2. 软引用
当内存不足, 会触发 JVM 的 GC, 如果 GC 后, 内存还是不足, 就会把软引用的包裹的对象给干掉, 也就是只有在内存不足, JVM 才会回收该对象

3. 弱引用
不管内存是否足够, 只要发生 GC, 都会被回收

4. 虚引用

  1. 无法通过虚引用来获取对一个对象的真实引用
  2. 虚引用必须与 ReferenceQueue 一起使用, 当 GC 准备回收一个对象, 如果发现它还有虚引用, 就会在回收之前, 把这个虚引用加入到与之关联的 ReferenceQueue 中
  3. 当发生 GC, 虚引用就会被回收, 并且会把回收的通知放到 ReferenceQueue 中

6 参考

并发容器之ThreadLocal
ThreadLocal源码深入剖析
强软弱虚引用, 只有体会过了, 才能记住

文章来源:https://blog.csdn.net/LCN29/article/details/135209833
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。