Java并发06 - ThreadLocal详解

news2025/1/18 9:38:58

ThreadLocal详解

文章目录

  • ThreadLocal详解
    • 一:认识 ThreadLocal 线程局部存储
      • 1:ThreadLocal特点
      • 2:如何实现线程隔离
      • 3:继承父线程的局部存储
      • 4:自动清理与内存泄漏问题
      • 5:ThreadLocal使用场景
      • 6:阿里巴巴编程规约
    • 二:ThreadLocal源码分析
      • 1:ThreadLocal属性
      • 2:ThreadLocal API
      • 3:InheritableThreadLocal工作原理
    • 三:ThreadLocalMap源码分析
      • 1:ThreadLocalMap属性
      • 2:构造方法
      • 3:开放寻址法
      • 4:获取方法getEntry
      • 5:添加方法set
      • 6:扩容方法rehash
      • 7:移除方法remove
      • 8:复杂度分析
    • 四:总结

一:认识 ThreadLocal 线程局部存储

1:ThreadLocal特点

ThreadLocal 提供了一种特殊的线程安全方式

使用 ThreadLocal 时,每个线程可以通过ThreadLocal#get或ThreadLocal#set方法访问资源在当前线程的副本,而不会与其他线程产生资源竞争

这意味着 ThreadLocal 并不考虑如何解决资源竞争,而是为每个线程分配独立的资源副本,从根本上避免发生资源冲突,是一种无锁的线程安全方法。

用一个表格总结 ThreadLocal 的 API:

public API描述
set(T)设置当前线程的副本
T get()获取当前线程的副本
void remove()移除当前线程的副本
ThreadLocal<S> withInitial(Supplier<S>)创建 ThreadLocal 并指定缺省值创建工厂
protected API描述
T initialValue()设置缺省值

2:如何实现线程隔离

ThreadLocal 在每个线程的 Thread 对象实例数据中分配独立的内存区域,当我们访问 ThreadLocal 时,本质上是在访问当前线程的 Thread 对象上的实例数据

不同线程访问的是不同的实例数据,因此实现线程隔离。

Thread 对象中这块数据就是一个使用线性探测的 ThreadLocalMap 散列表,ThreadLocal 对象本身就作为散列表的 Key ,而 Value 是资源的副本

当我们访问 ThreadLocal 时,就是先获取当前线程实例数据中的 ThreadLocalMap 散列表,再通过当前 ThreadLocal 作为 Key 去匹配键值对

// 获取当前线程的副本
public T get() {
    // 先获取当前线程实例数据中的 ThreadLocalMap 散列表
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 通过当前 ThreadLocal 作为 Key 去匹配键值对
    ThreadLocalMap.Entry e = map.getEntry(this);
}

// 获取线程 t 的 threadLocals 字段,即 ThreadLocalMap 散列表
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 静态内部类
static class ThreadLocalMap {
    // 详细源码分析见下文 ...
}
// Thread 对象的实例数据
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

// 线程退出之前,会置空threadLocals变量,以便随后GC
private void exit() {
    // ...
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    // ...
}

示意图

在这里插入图片描述

3:继承父线程的局部存储

在业务开发的过程中,我们可能希望子线程可以访问主线程中的 ThreadLocal 数据,然而 ThreadLocal 是线程隔离的,包括在父子线程之间也是线程隔离的

为此,ThreadLocal 提供了一个相似的子类 InheritableThreadLocal

ThreadLocal 和 InheritableThreadLocal 分别对应于线程对象上的两块内存区域

  • ThreadLocal 字段: 在所有线程间隔离;
  • InheritableThreadLocal 字段: 子线程会继承父线程的

在 InheritableThreadLocal 中,可以重写 childValue() 方法修改拷贝到子线程的数据

在这里插入图片描述

InhertableThreadLocal区域在拷贝后依然是线程隔离的,子线程的写不会影响到父线程,反之亦然
拷贝过程是在父线程执行的,虽然方法写在子线程中,但是是在父线程中执行的,子线程调用start()后执行

在这里插入图片描述

4:自动清理与内存泄漏问题

在这里插入图片描述

ThreadLocal 提供具有自动清理数据的能力,具体分为 2 个颗粒度:

  • 自动清理散列表:ThreadLocal 数据是 Thread 对象的实例数据,当线程执行结束后,就会跟随 Thread 对象 GC 而被清理
  • 自动清理无效的键值对:ThreadLocal 是使用弱键的动态散列表,当 Key 对象不再被持有强引用时,垃圾收集器会按照弱引用策略自动回收 Key 对象,并在下次访问 ThreadLocal 时清理无效键值对。

在这里插入图片描述

引用关系的示意图

在这里插入图片描述

自动清理无效键值对会存在 “滞后性”,在滞后的这段时间内,无效的键值对数据没有及时回收,就发生内存泄漏

  • 如果创建 ThreadLocal 的线程一直持续运行,整个散列表的数据就会一致存在。比如线程池中的线程是复用的,这部分复用线程中的 ThreadLocal 数据就不会被清理
  • 如果在数据无效后没有再访问过 ThreadLocal 对象,那么自然就没有机会触发清理
  • 即使访问 ThreadLocal 对象,也不一定会触发清理

所以,虽然 ThreadLocal 提供了自动清理无效数据的能力,但是为了避免内存泄漏,在业务开发中应该及时调用 ThreadLocal的remove() 清理无效的局部存储

/**
 * Remove the entry for key.
 */
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) {
            e.clear(); // 清理
            expungeStaleEntry(i);
            return;
        }
    }
}

5:ThreadLocal使用场景

场景 1 - 无锁线程安全:ThreadLocal 提供了一种特殊的线程安全方式,从根本上避免资源竞争,也体现了空间换时间的思想;

场景 2 - 线程级别单例: 一般的单例对象是对整个进程可见的,使用 ThreadLocal 也可以实现线程级别的单例;

场景 3 - 共享参数:如果一个模块有非常多地方需要使用同一个变量,相比

6:阿里巴巴编程规约

在《阿里巴巴 Java 开发手册》中,亦有关于 ThreadLocal API 的编程规约:

【强制规约1】

SimpleDateFormate 是线程不安全的类,一般不要定义为 static 变量。

如果定义为 static,必须加锁,或者使用 DateUtils 工具类(使用 ThreadLocal 做线程隔离)。

package com.cui.commonboot.onlytest;

import com.alibaba.excel.util.DateUtils;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Test05 {
    
    // 使用ThreadLocal做隔离
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    public static void main(String[] args) {
        String format = df.get().format(new Date());
        System.out.println(format);
    }
}

DateUtils底层

在这里插入图片描述

【强制规约2】

必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常被反复用,如果不清理自定义的 ThreadLocal 变量,则可能会影响后续业务逻辑和造成内存泄漏等问题。

尽量在代码中使用 try-finally 块回收,在 finally 中调用 remove() 方法。

【参考规约】

ThreadLocal 变量建议使用 static 全局变量,可以保证变量在类初始化时创建,所有类实例可以共享同一个静态变量

二:ThreadLocal源码分析

1:ThreadLocal属性

ThreadLocal 只有一个 threadLocalHashCode 散列值属性:

  • ThreadLocalHashCode 相当于 ThreadLocal 的自定义散列值,在创建 ThreadLocal 对象时,会调用 nextHashCode() 方法分配一个散列值
  • ThreadLocal 每次调用 nextHashCode() 方法都会将散列值追加 HASH_INCREMENT,并记录在一个全局的原子整型 nextHashCode
  • ThreadLocal 的散列值序列为:0、HASH_INCREMENT、HASH_INCREMENT * 2、HASH_INCREMENT * 3、…
public class ThreadLocal<T> {

    // 为什么 ThreadLocal 不重写 hashCode()
    // ThreadLocal 的散列值,类似于重写 Object#hashCode()
    private final int threadLocalHashCode = nextHashCode();

    // 全局原子整型,每调用一次 nextHashCode() 累加一次
    private static AtomicInteger nextHashCode = new AtomicInteger();

    // 为什么 ThreadLocal 散列值的增量是 0x61c88647?
    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        // 返回上一次 nextHashCode 的值,并累加 HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

static class ThreadLocalMap {
    ...;
}

🙋🏻‍♀️疑问 1:为什么 ThreadLocal 不重写 hashCode()?

如果重写 Object#hashCode(),那么 threadLocalHashCode 散列值就会对所有散列表生效。

而 threadLocalHashCode 散列值是专门针对数组为 2 的整数幂的散列表设计的,在其他散列表中不一定表现良好。

因此 ThreadLocal 没有重写 Object#hashCode(),让 threadLocalHashCode 散列值只在 ThreadLocal 内部的 ThreadLocalMap 使用。

public class ThreadLocal<T> {

// ThreadLocal 未重写 hashCode(),因为这样会对所有的散列表生效
@Override
public int hashCode() {
return threadLocalHashCode;
}
}

🙋🏻‍♀️疑问 2:为什么使用 ThreadLocal 作为散列表的 Key,而不是常规思维用 Thread Id 作为 Key?

如果使用 Thread Id 作为 Key,那么就需要在每个 ThreadLocal 对象中维护散列表,而不是每个线程维护一个散列表。

此时,当多个线程并发访问同一个 ThreadLocal 对象中的散列表时,就需要通过加锁保证线程安全。

而 ThreadLocal 的方案让每个线程访问独立的散列表,就可以从根本上规避线程竞争。

2:ThreadLocal API

在这里插入图片描述

在 ThreadLocal 的 API 会通过 getMap() 方法获取当前线程的 Thread 对象中的 threadLocals 字段,这是线程隔离的关键

public ThreadLocal() {
    // do nothing
}

// 子类可重写此方法设置缺省值(方法命名为 defaultValue 获取更贴切)
protected T initialValue() {
    // 默认不提供缺省值
    return null;
}

// 帮助方法:不重写 ThreadLocal 也可以设置缺省值
// supplier:缺省值创建工厂
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

// 1. 获取当前线程的副本
public T get() {
    Thread t = Thread.currentThread();
    // getMap线程隔离的关键
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 存在匹配的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    // 未命中,则获取并设置缺省值(即缺省值采用懒初始化策略)
    return setInitialValue();
}

// 获取并设置缺省值
private T setInitialValue() {
    T value = initialValue();
    // 其实源码中是并不是直接调用set(),而是复制了一份 set() 方法的源码
    // 这是为了防止子类重写 set() 方法后改变缺省值逻辑
    set(value);
    return value;
}
  
// 2. 设置当前线程的副本
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 直到设置值的时候才创建(即 ThreadLocalMap 采用懒初始化策略)
        createMap(t, value);
}

// 3. 移除当前线程的副本
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}


// ====> 线程隔离关键
ThreadLocalMap getMap(Thread t) {
    // 重点:获取当前线程的 threadLocals 字段
    return t.threadLocals;
}

// ThreadLocal 缺省值帮助类
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    // 重写 initialValue() 以设置缺省值
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

3:InheritableThreadLocal工作原理

父线程在创建子线程时,在子线程的构造方法中会批量将父线程的有效键值对数据拷贝到子线程,因此子线程可以复用父线程的局部存储。

// Thread 对象的实例数据
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

// 构造方法
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 拷贝父线程的 InheritableThreadLocal 散列表
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
}
// 带 Map 的构造方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

static class ThreadLocalMap {
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Object value = key.childValue(e.value);
        ...
    }	
}

InheritableThreadLocal 在拷贝父线程散列表的过程中,会调用 InheritableThreadLocal#childValue() 尝试转换为子线程需要的数据

默认是直接传递,可以重写这个方法修改拷贝的数据。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    // 参数:父线程的数据
    // 返回值:拷贝到子线程的数据,默认为直接传递
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ...;
}

三:ThreadLocalMap源码分析

ThreadLocalMap 是 ThreadLocal 内部使用的散列表,也是 ThreadLocal 的静态内部类

1:ThreadLocalMap属性

在这里插入图片描述
可以看到,散列表必备底层数组 table、键值对数量 size、扩容阈值 threshold 等属性都有,并且也要求数组的长度是 2 的整数倍。

主要区别在于 Entry 节点上:
在这里插入图片描述
🙋🏻‍♀️疑问 4:为什么 Key 是弱引用,而不是 Entry 或 Value 是弱引用

首先,Entry 一定要持有强引用,而不能持有弱引用。这是因为 Entry 是 ThreadLocalMap 内部维护数据结构的实现细节,并不会暴露到 ThreadLocalMap 外部,即除了 ThreadLocalMap 本身之外没有其它地方持有 Entry 的强引用。所以,如果持有 Entry 的弱引用,即使 ThreadLocalMap 外部依然在使用 Key 对象,ThreadLocalMap 内部依然会回收键值对,这与预期不符。

其次,不管是 Key 还是 Value 使用弱引用都可以实现自动清理,至于使用哪一种方法各有优缺点,适用场景也不同。

Key 弱引用的优点是外部不需要持有 Value 的强引用,缺点是存在 “重建 Key 不等价” 问题

由于 ThreadLocal 的应用场景是线程局部存储,我们没有重建多个 ThreadLocal 对象指向同一个键值对的需求,也没有重写 Object#equals() 方法,所以不存在重建 Key 的问题,使用 Key 弱引用更方便。

类型优点缺点场景
Key 弱引用外部不需要持有 Value 的强引用,使用更简单重建 Key 不等价未重写 equals
Value 弱引用重建 Key 等价外部需要持有 Value 的强引用重写 equals

2:构造方法

ThreadLocalMap 有 2 个构造方法:

  • 带首个键值对的构造方法: 在首次添加元素或首次查询数据生成缺省值时,才会调用此构造方法创建 ThreadLocalMap 对象,并添加首个键值对;
  • 带 Map 的构造方法: 在创建子线程时,父线程会调用此构造方法创建 ThreadLocalMap 对象,并添加批量父线程 ThreadLocalMap 中的有效键值对。
// 带首个键值对的构造方法
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// 带 Map 的构造方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

static class ThreadLocalMap {

    // -> 带首个键值对的构造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 创建底层数组(默认长度为 16)
        table = new Entry[INITIAL_CAPACITY];
        // 散列值转数组下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 添加首个元素(首个元素一定不会冲突)
        table[i] = new Entry(firstKey, firstValue);
        // 键值对数量
        size = 1;
        // 设置扩容阈值
        setThreshold(INITIAL_CAPACITY);
    }

    // -> 带 Map 的构造方法
    // 创建子线程的时候用这个
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        // 设置扩容阈值
        setThreshold(len);
        // 创建底层数组(使用 parent 的长度)
        table = new Entry[len];

        // 逐个添加键值对
        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                // 如果键值对的 Key 被回收,则跳过
                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++;
                }
            }
        }
    }
}

3:开放寻址法

  • 添加键值对: 先将散列值取余映射到数组下标,然后从数组下标位置开始探测与目标 Key 相等的节点。
    • 如果找到,则将旧 Value 替换为新 Value,否则沿着数组顺序线性探测。直到线性探测遇到空闲位置,则说明节点不存在,需要添加新节点。
    • 如果在添加键值对后数组没有空闲位置,就触发扩容;
  • 查找键值对: 查找类似。
    • 也是先将散列值映射到数组下标,然后从数组下标位置开始线性探测。
    • 直到线性探测遇到空闲位置,则说明节点不存在;
  • 删除键值对: 删除类似。
    • 由于查找操作在遇到空闲位置时,会认为键值对不存在于散列表中,如果删除操作时 “真删除”,就会使得一组连续段产生断层,导致查找操作失效。
    • 因此,删除操作要做 “假删除”,删除操作只是将节点标记为 “Deleted”,查找操作在遇到 “Deleted” 标记的节点时会继续向下探测。

在这里插入图片描述
可以看到,在线性探测中的 “连续段” 非常重要:

线性探测在判断节点是否存在于散列表时,并不是线性遍历整个数组,而只会线性遍历从散列值映射的数组下标后的连续段。

4:获取方法getEntry

在这里插入图片描述

static class ThreadLocalMap {
		
    // 获取 Key 匹配的键值对
    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);
    }

    // -> 线性探测,并且清理连续段中无效数据
    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)
                // Key 对象被回收,触发连续段清理
                // 连续段清理在一个 while 循环中只会触发一次,因为这个段中 k == null 的节点都被清理出去了
                // 如果连续段清理后,i 位置为 null,那么目标节点一定不存在
                expungeStaleEntry(i);
            else
                // 未命中,探测下一个位置
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

    // -> 清理连续段中无效数据
    // staleSlot:起点
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 清理无效节点(起点一定是无效节点)
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // 线性探测直到遇到空闲位置
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // 清理无效节点
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // 再散列有效节点
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

    // -> 线性探测下一个数组位置
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
}

🙋🏻‍♀️疑问 5:为什么要对有效节点再散列呢?

线性探测只会遍历连续段,而清理无效节点会导致连续段产生断层。

如果没有对有效节点做再散列,那么有效节点在下次查询时就有可能探测不到了。

5:添加方法set

流程非常复杂,主要步骤概括为 6 步:

  1. 先将散列值映射到数组下标,并且开始线性探测;
  2. 如果探测中遇到目标节点,则将旧 Value 更新为新 Value;
  3. 如果探测中遇到无效节点,则会调用 replaceStaleEntry() 清理连续段并添加键值对;
  4. 如果未探测到目标节点或无效节点,则创建并添加新节点;
  5. 添加新节点后调用 cleanSomeSlots() 方法清理部分数据;
  6. 如果没有发生清理并且达到扩容阈值,则触发 rehash() 扩容。

在这里插入图片描述

static class ThreadLocalMap {

    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        // 1、散列值转数组下标
        int i = key.threadLocalHashCode & (len-1);

        // 线性探测
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            if (k == key) {
                // 2、命中,将旧 Value 替换为新 Value
                e.value = value;
                return;
            }

            if (k == null) {
                // 3、清理无效节点,并插入键值对
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // 4、如果未探测到目标节点或无效节点,则创建并添加新节点
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // cleanSomeSlots:清理部分数据
        // 5、添加新节点后调用 cleanSomeSlots() 方法清理部分数据
        if (!cleanSomeSlots(i, sz /*有效数据个数*/) && sz >= threshold)
            // 6、如果没有发生清理并且达到扩容阈值,则触发 rehash() 扩容
            rehash();
    }

    // -> 3、清理无效节点,并插入键值对
    // key-value:插入的键值对
    private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        // slotToExpunge:记录清理的起点
        int slotToExpunge = staleSlot;
        // 3.1 向前探测找到连续段中的第一个无效节点
        for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        // 3.2 向后探测目标节点
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                // 3.2.1 命中,将目标节点替换到 staleSlot 位置
                e.value = value;
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // 3.2.2 如果连续段在 staleSlot 之前没有无效节点,则从 staleSlot 的下一个无效节点开始清理
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 3.2.3 如果连续段中还有其他无效节点,则清理
                // expungeStaleEntry:连续段清理
                // cleanSomeSlots:对数式清理
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            // 如果连续段在 staleSlot 之前没有无效节点,则从 staleSlot 的下一个无效节点开始清理
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 3.3 创建新节点并插入 staleSlot 位置
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 3.4 如果连续段中还有其他无效节点,则清理
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len /*数组长度*/);
    }

    // 5、对数式清理
    // i:起点
    // n:数组长度或有效数据个数
    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];
            if (e != null && e.get() == null) {
                // 发现无效节点,重新探测 log2(len)
                n = len;
                removed = true;
                // 连续段清理
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0); // 探测 log2(len)
        return removed;
    }
}

6:扩容方法rehash

在添加方法中,如果添加键值对后散列值的长度超过扩容阈值,就会调用 rehash() 方法扩容,主体流程分为 3步:

  1. 先完整扫描散列表清理无效数据,清理后用较低的阈值判断是否需要扩容;
  2. 创建新数组;
  3. 将旧数组上无效的节点忽略,将有效的节点再散列到新数组上。

在这里插入图片描述

static class ThreadLocalMap {

    // 扩容(在容量到达 threshold 扩容阈值时调用)
    private void rehash() {
        // 1、全数组清理
        expungeStaleEntries();
		
        // 2、用较低的阈值判断是否需要扩容
        if (size >= threshold - threshold / 4)
            // 3、真正执行扩容
            resize();
    }

    // -> 1、完整散列表清理
    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)
                // 很奇怪为什么不修改 j 指针
                expungeStaleEntry(j);
        }
    }

    // -> 3、真正执行扩容
    private void resize() {
        Entry[] oldTab = table;
        // 扩容为 2 倍
        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) {
                    // 清除无效键值的 Value
                    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;
    }
}

🎉 可见只有扩容会触发完整散列表清理,其他情况都不能保证清理,甚至不会触发

7:移除方法remove

ThreadLocalMap 的移除方法是添加方法的逆运算,ThreadLocalMap 也没有做动态缩容

与常规的移除操作不同的是,ThreadLocalMap 在删除时会执行 expungeStaleEntry() 清除无效节点,并对连续段中的有效节点做再散列

所以 ThreadLocalMap 是 “真删除”。

static class ThreadLocalMap {

    // 移除
    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) {
                // 清除弱引用关系
                e.clear();
                // 清理连续段
                expungeStaleEntry(i);
                return;
            }
        }
    }
}

8:复杂度分析

总结下 ThreadLocalMap 的时间复杂度,以下 K 为连续段的长度,N 是数组长度。

  • 获取方法: 平均时间复杂度为 O(K);
  • 添加方法: 平均时间复杂度为 O(K),在触发扩容的添加操作中时间复杂度为 O(N),基于摊还分析后时间复杂度依然是 O(K);
  • 移除方法: 移除是 “真删除”,平均时间复杂度为 O(K)。

四:总结

1、ThreadLocal 是一种特殊的无锁线程安全方式,通过为每个线程分配独立的资源副本,从根本上避免发生资源冲突;

2、ThreadLocal 在所有线程间隔离,InheritableThreadLocal 在创建子线程时会拷贝父线程中 InheritableThreadLocal 的有效键值对

3、虽然 ThreadLocal 提供了自动清理数据的能力,但是自动清理存在滞后性。为了避免内存泄漏,在业务开发中应该及时调用 remove 清理无效的局部存储

4、ThreadLocal 是采用线性探测解决散列冲突的散列表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2278409.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【设计模式】 单例模式(单例模式哪几种实现,如何保证线程安全,反射破坏单例模式)

单例模式 作用&#xff1a;单例模式的核心是保证一个类只有一个实例&#xff0c;并且提供一个访问实例的全局访问点。 实现方式优缺点饿汉式线程安全&#xff0c;调用效率高 &#xff0c;但是不能延迟加载懒汉式线程安全&#xff0c;调用效率不高&#xff0c;能延迟加载双重检…

【漏洞预警】FortiOS 和 FortiProxy 身份认证绕过漏洞(CVE-2024-55591)

文章目录 一、产品简介二、漏洞描述三、影响版本四、漏洞检测方法五、解决方案 一、产品简介 FortiOS是Fortinet公司核心的网络安全操作系统&#xff0c;广泛应用于FortiGate下一代防火墙&#xff0c;为用户提供防火墙、VPN、入侵防御、应用控制等多种安全功能。 FortiProxy则…

记录一次 centos 启动失败

文章目录 现场1分析1现场2分析2搜索实际解决过程 现场1 一次断电,导致 之前能正常启动的centos 7.7 起不来了有部分log , 关键信息如下 [1.332724] XFS(sda3): Internal error xfs ... at line xxx of fs/xfs/xfs_trans.c [1.332724] XFS(sda3): Corruption of in-memory data…

关于vite+vue3+ts项目中env.d.ts 文件详解

env.d.ts 文件是 Vite 项目中用于定义全局类型声明的 TypeScript 文件。它帮助开发者向 TypeScript提供全局的类型提示&#xff0c;特别是在使用一些特定于 Vite 的功能时&#xff08;如 import.meta.env&#xff09;。以下是详细讲解及代码示例 文章目录 **1. env.d.ts 文件的…

虚拟专用网VPN的概念及实现VPN的关键技术

虚拟专用网VPN通过建立在公共网络上的重要通道(1分),实现远程用户、分支机构、业务伙伴等与机构总部网络的安全连接&#xff0c;从而构建针对特定组织机构的专用网络&#xff0c;实现与专用网络类似的功能&#xff0c;可以达到PN安全性的目的&#xff0c;同时成本相对要低很多(…

将 AzureBlob 的日志通过 Azure Event Hubs 发给 Elasticsearch(1)

问题 项目里使用了 AzureBlob 存储了用户上传的各种资源文件&#xff0c;近期 AzureBlob 的流量费用增长很快&#xff0c;想通过分析Blob的日志&#xff0c;获取一些可用的信息&#xff0c;所以有了这个需求&#xff1a;将存储账户的日志&#xff08;读写&#xff0c;审计&…

X-12-ARIMA:季节性调整(Seasonal Adjustment)的强大工具,介绍数学原理

X-12-ARIMA&#xff1a;季节性调整的强大工具 在经济学、金融学以及各类统计数据分析中&#xff0c;季节性调整&#xff08;Seasonal Adjustment&#xff09;是一个至关重要的步骤。许多经济指标&#xff0c;如GDP、失业率和零售销售数据等&#xff0c;往往会受到季节性因素的…

.netframwork模拟启动webapi服务并编写对应api接口

在.NET Framework环境中模拟启动Web服务&#xff0c;可以使用几种不同的方法。一个常见的选择是利用HttpListener类来创建一个简单的HTTP服务器&#xff0c;或者使用Owin/Katana库来自托管ASP.NET Web API或MVC应用。下面简要介绍Owin/Katana示例代码。这种方法更加灵活&#x…

网络安全构成要素

一、防火墙 组织机构内部的网络与互联网相连时&#xff0c;为了避免域内受到非法访问的威胁&#xff0c;往往会设置防火墙。 使用NAT&#xff08;NAPT&#xff09;的情况下&#xff0c;由于限定了可以从外部访问的地址&#xff0c;因此也能起到防火墙的作用。 二、IDS入侵检…

免费送源码:Java+ssm+MySQL 基于PHP在线考试系统的设计与实现 计算机毕业设计原创定制

摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对在线考试等问题&#xff0c;对如何通过计算…

html中鼠标位置信息

pageX&#xff1a;鼠标距离页面的最左边的距离&#xff0c;包括滚动条的长度。clientX&#xff1a;鼠标距离浏览器视口的左距离&#xff0c;不包括滚动条。offsetX&#xff1a;鼠标到事件源左边的距离。movementX&#xff1a;鼠标这次触发的事件的位置相对于上一次触发事件的位…

光谱相机的光谱分辨率可以达到多少?

多光谱相机 多光谱相机的光谱分辨率相对较低&#xff0c;波段数一般在 10 到 20 个左右&#xff0c;光谱分辨率通常在几十纳米到几百纳米之间&#xff0c;如常见的多光谱相机光谱分辨率为 100nm 左右。 高光谱相机 一般的高光谱相机光谱分辨率可达 2.5nm 到 10nm 左右&#x…

RAG 切块Chunk技术总结与自定义分块实现思路

TrustRAG项目地址&#x1f31f;&#xff1a;https://github.com/gomate-community/TrustRAG 可配置的模块化RAG框架 切块简介 在RAG&#xff08;Retrieval-Augmented Generation&#xff09;任务中&#xff0c;Chunk切分是一个关键步骤&#xff0c;尤其是在处理结构复杂的PDF文…

Java基础——概念和常识(语言特点、JVM、JDK、JRE、AOT/JIT等介绍)

我是一个计算机专业研0的学生卡蒙Camel&#x1f42b;&#x1f42b;&#x1f42b;&#xff08;刚保研&#xff09; 记录每天学习过程&#xff08;主要学习Java、python、人工智能&#xff09;&#xff0c;总结知识点&#xff08;内容来自&#xff1a;自我总结网上借鉴&#xff0…

Low-Level 大一统:如何使用Diffusion Models完成视频超分、去雨、去雾、降噪等所有Low-Level 任务?

Diffusion Models专栏文章汇总&#xff1a;入门与实战 前言&#xff1a;视频在传输过程中常常因为各种因素&#xff08;如恶劣天气、噪声、压缩和传感器分辨率限制&#xff09;而出现质量下降&#xff0c;这会严重影响计算机视觉任务&#xff08;如目标检测和视频监控&#xff…

矩阵碰一碰发视频源码技术开发全解析,支持OEM

在当今数字化内容传播迅速发展的时代&#xff0c;矩阵碰一碰发视频功能以其便捷、高效的特点&#xff0c;为用户分享视频提供了全新的体验。本文将深入探讨矩阵碰一碰发视频源码的技术开发过程&#xff0c;从原理到实现&#xff0c;为开发者提供全面的技术指引。 一、技术原理 …

测试工程师的linux 命令学习(持续更新中)

1.ls """1.ls""" ls -l 除文件名称外&#xff0c;亦将文件型态、权限、拥有者、文件大小等资讯详细列出 ls -l等同于 ll第一列共10位&#xff0c;第1位表示文档类型&#xff0c;d表示目录&#xff0c;-表示普通文件&#xff0c;l表示链接文件。…

HJ34 图片整理(Java版)

一、试题地址 图片整理_牛客题霸_牛客网 二、试题描述 描述 对于给定的由大小写字母和数字组成的字符串&#xff0c;请按照 ASCIIASCII 码值将其从小到大排序。 如果您需要了解更多关于 ASCIIASCII 码的知识&#xff0c;请参考下表。 输入描述&#xff1a; 在一行上输入一…

web开发工具之:三、JWT的理论知识,java的支持,封装的工具类可以直接使用

文章目录 前言一、JWT的理论知识1. 什么是 JWT&#xff08;JSON Web Token&#xff09;&#xff1f;2. **JWT 的组成**3. **JWT 的特点**4. **JWT 的使用场景**5. **JWT 的生命周期**6. **JWT 的优点**7. **JWT 的注意事项**5. **JWT 示例**总结 二、java的springboot支持1. po…

电路笔记(信号):Python 滤波器设计分析工具pyfda

目录 滤波器设置(3步实现滤波器设计)数据分析与使用pyfda功能界面数字滤波器数学表示线性相位线性相位的定义线性相位的特性 冲击响应quartus数据加载 CG 滤波器设置(3步实现滤波器设计) pip install pyfda #安装python依赖&#xff0c;详见https://pyfda.readthedocs.io/en/la…