多线程 | ThreadLocal源码分析

2024-09-07 13:20

本文主要是介绍多线程 | ThreadLocal源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1. ThreadLocal解决了什么问题
    • 数据隔离
    • 避免参数传递
    • 资源管理
  • 2. ThreadLocal和Synchronized
  • 3. ThreadLocal核心
    • 核心特性
    • 常见方法
    • 使用场景
    • 注意事项
  • 4. ThreadLocal如何实现线程隔离的?(重点)
    • ThreadLocal 的自动清理与内存泄漏问题
    • 阿里巴巴 ThreadLocal 编程规约
  • 5. ThreadLocal源码分析
    • ThreadLocal.java
    • Thread.java
    • ThreadLocalMap 源码分析
      • ThreadLocalMap 的获取方法
      • ThreadLocalMap 的添加方法
      • ThreadLocalMap 的扩容方法
      • ThreadLocalMap 的移除方法
      • ThreadLocalMap 复杂度分析
      • 访问 ThreadLocal 一定会清理无效数据吗?
  • 6. 总结
  • 参考资料

1. ThreadLocal解决了什么问题

数据隔离

在多线程环境下,不同线程可能需要操作不同的对象实例,避免数据的相互干扰。ThreadLocal为每个线程提供独立的数据副本,使得各线程之间的数据相互隔离
例如,在一个 Web 应用中,可能需要记录每个用户的请求信息。如果使用全局变量来存储这些信息,不同用户的请求可能会相互覆盖。而使用ThreadLocal,可以为每个用户的请求线程创建一个独立的存储区域,存储该用户的请求信息,避免了数据的混乱。

避免参数传递

在一些复杂的系统中,可能需要在多个方法之间传递一些上下文信息。如果通过方法参数的方式进行传递,会使得代码变得冗长且难以维护。ThreadLocal可以将这些上下文信息存储在当前线程中,在需要的地方直接获取,无需进行繁琐的参数传递。
例如,在一个事务处理系统中,需要在多个方法中获取当前事务的 ID。如果不使用ThreadLocal,可能需要在每个方法中都传递事务 ID 作为参数。而使用ThreadLocal,可以在事务开始时将事务 ID 存储在当前线程中,在需要的地方直接从ThreadLocal中获取事务 ID,简化了代码。

资源管理

在一些情况下,需要为每个线程分配独立的资源,如数据库连接、文件句柄等。使用ThreadLocal可以方便地管理这些资源,确保每个线程都有自己独立的资源副本,避免资源的竞争和冲突。
例如,在一个数据库访问系统中,为了提高性能,可以为每个线程分配一个独立的数据库连接。使用ThreadLocal可以在需要的时候获取当前线程的数据库连接,在使用完毕后及时关闭,避免了数据库连接的频繁创建和销毁,提高了系统的性能。
总之,ThreadLocal在 Java 中提供了一种方便的方式来实现线程局部变量,解决了多线程环境下的数据隔离、参数传递和资源管理等问题,使得多线程编程更加简洁、高效和可靠。

2. ThreadLocal和Synchronized

ThreadLocal专注不同线程之间的数据相互隔离,对变量的操作只在当前线程内可见,不会影响其他线程。适用于需要为每个线程保存独立数据的场景,不存在阻塞问题,但变量过多会占用较多的内存空间,需要注意内存泄漏问题。
synchronized通过对临界区(一段需要同步的代码块或方法)加锁来保证同一时刻只有一个线程能够访问该区域。主要用于解决多个线程对共享资源的并发访问问题,确保数据的一致性和完整性。会导致线程的阻塞和唤醒,有一定的性能开销。

3. ThreadLocal核心

ThreadLocal是Java中的一个类,它提供了线程局部(thread-local)变量。这些变量与普通的变量不同,因为每个访问变量的线程都有其自己独立初始化的变量副本。通过ThreadLocal实例,可以隔离并保存每个线程的数据,确保线程之间不会相互干扰,避免因并发访问导致的数据不一致问题。

核心特性

线程隔离:每个线程对 ThreadLocal 变量的修改对其他线程是不可见的。
无继承性:子线程不能访问父线程的 ThreadLocal 变量,除非子线程中有显式的设置或复制操作。
避免同步:由于每个线程都有自己的变量副本,因此不需要同步就可以保证线程安全。

常见方法

public T get():返回当前线程对应的变量的值。如果当前线程没有对应的值,则返回初始值或 null(如果未设置初始值)。
public void set(T value):设置当前线程对应的变量的值。
public void remove():删除当前线程对应的变量。
protected T initialValue():这是一个受保护的方法,用于设置变量的初始值。通常,你可以通过匿名内部类来覆盖这个方法。

使用场景

数据库连接:在多线程应用中,每个线程可能需要自己的数据库连接。使用 ThreadLocal 可以为每个线程保存其自己的连接。
会话管理:在 Web 应用中,每个用户的会话数据可以使用 ThreadLocal 存储,从而确保同一用户的多个请求在同一个线程中处理时能够访问到正确的会话数据。
线程内上下文传递:有时需要在同一个线程的不同方法之间传递一些上下文信息,而不希望使用全局变量或参数传递。这时可以使用 ThreadLocal。

注意事项

  1. 内存泄漏:如果线程不再需要使用该变量,但忘记调用 remove() 方法来清理,那么由于 ThreadLocalMap 中的 Entry 的 key 是对 Thread 的弱引用,所以 Thread 被回收后,Entry 的 key 会被置为 null,但 value 不会被回收,从而导致内存泄漏。因此,使用完 ThreadLocal 后,最好调用 remove() 方法来清理。
  2. 线程池中的使用:在线程池中,线程可能会被复用。如果线程之前设置过 ThreadLocal 变量,但在使用后没有清理,那么下一个任务可能会读取到上一个任务设置的值。因此,在线程池中使用 ThreadLocal 时需要特别小心。
  3. 初始化问题:如果不重写 initialValue() 方法,并且在使用前没有调用 set() 方法设置值,那么 get() 方法将返回 null。为了避免这种情况,可以重写 initialValue() 方法来提供一个默认值。
  4. 不适用于全局共享状态:虽然 ThreadLocal 可以在多个线程之间隔离数据,但它不适用于需要在多个线程之间共享和修改的全局状态。对于这种情况,应该使用其他同步机制(如锁或原子变量)。

4. ThreadLocal如何实现线程隔离的?(重点)

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

Thread 对象中这块数据就是一个使用线性探测的 ThreadLocalMap 散列表,ThreadLocal 对象本身就作为散列表的 Key ,而 Value 是资源的副本。当我们访问 ThreadLocal 时,就是先获取当前线程实例数据中的 ThreadLocalMap 散列表,再通过当前 ThreadLocal 作为 Key 去匹配键值对。
在这里插入图片描述
在这里插入图片描述
ThreadLocal的工作原理主要是通过每个线程内部的ThreadLocalMap来实现的。ThreadLocalMap是ThreadLocal的静态内部类,它实现了类似于Map的键值对存储结构,但是键是弱引用(WeakReference)类型的ThreadLocal对象,而值则是与线程相关的数据。

每个线程都有一个名为threadLocals的成员变量,这个变量就是ThreadLocalMap类型的。当线程调用ThreadLocal的set()方法时,它会将ThreadLocal对象和要存储的值作为键值对添加到自己的threadLocals中。当调用get()方法时,线程会从自己的threadLocals中根据ThreadLocal对象查找对应的值。

由于每个线程都有自己的threadLocals,因此它们之间不会共享这些线程局部变量的值。这就是ThreadLocal能够实现线程隔离的原因。

ThreadLocal 的自动清理与内存泄漏问题

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

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

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

场景1: 如果创建 ThreadLocal 的线程一直持续运行,整个散列表的数据就会一致存在。比如线程池中的线程(大体)是复用的,这部分复用线程中的 ThreadLocal 数据就不会被清理;
场景2: 如果在数据无效后没有再访问过 ThreadLocal 对象,那么自然就没有机会触发清理;
场景3: 即使访问 ThreadLocal 对象,也不一定会触发清理(原因见下文源码分析)。
综上所述:虽然 ThreadLocal 提供了自动清理无效数据的能力,但是为了避免内存泄漏,在业务开发中应该及时调用 ThreadLocal#remove 清理无效的局部存储。

阿里巴巴 ThreadLocal 编程规约

在《阿里巴巴 Java 开发手册》中,亦有关于 ThreadLocal API 的编程规约:
【强制】 SimpleDateFormate 是线程不安全的类,一般不要定义为 static ****变量。如果定义为 static,必须加锁,或者使用 DateUtils 工具类(使用 ThreadLocal 做线程隔离)。

private static final ThreadLocal<DataFormat> df = new ThreadLocal<DateFormat>(){// 设置缺省值 / 初始值@Overrideprotected DateFormat initialValue(){return new SimpleDateFormat("yyyy-MM-dd");}
};// 使用:
DateUtils.df.get().format(new Date());

【参考】 ThreadLocal 变量建议使用 static 全局变量,可以保证变量在类初始化时创建,所有类实例可以共享同一个静态变量(例如,在 Android Looper 的案例中,ThreadLocal 就是使用 static 修饰的全局变量)。
【强制】 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常被反复用,如果不清理自定义的 ThreadLocal 变量,则可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代码中使用 try-finally 块回收,在 finally 中调用 remove() 方法。

5. ThreadLocal源码分析

用一个表格总结 ThreadLocal 的 API:
在这里插入图片描述

ThreadLocal.java

public class ThreadLocal < T > {// 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_INCREMENTreturn nextHashCode.getAndAdd(HASH_INCREMENT);}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();// ThreadLocalMap 详细源码分析见下文ThreadLocalMap map = getMap(t);if (map != null) {// 存在匹配的EntryThreadLocalMap.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() 以设置缺省值@Overrideprotected T initialValue() {return supplier.get();}}
}

Thread.java

// Thread 对象的实例数据
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;// 线程退出之前,会置空threadLocals变量,以便随后GC
private void exit() {// ...threadLocals = null;inheritableThreadLocals = null;inheritedAccessControlContext = null;// ...
}

ThreadLocalMap 源码分析

在这里插入图片描述
可以看到,散列表必备底层数组 table、键值对数量 size、扩容阈值 threshold 等属性都有,并且也要求数组的长度是 2 的整数倍。主要区别在于 Entry 节点上:
1、ThreadLocal 本身就是散列表的键 Key;
2、扩容阈值为数组容量的 2/3;
3、ThreadLocalMap#Entry 节点没有 next 指针,因为 ThreadLocalMap 采用线性探测解决散列冲突,所以不存在链表指针;
4、ThreadLocalMap#Entry 在键值对的 Key 上使用弱引用,这与 WeakHashMap 相似。

static class ThreadLocalMap {// 默认数组容量(容量必须是 2 的整数倍)private static final int INITIAL_CAPACITY = 16;// 底层数组private Entry[] table;// 有效键值对数量private int size = 0;// 扩容阈值private int threshold; // Default to 0private void setThreshold(int len) {threshold = len * 2 / 3;}// 键值对节点static class Entry extends WeakReference<ThreadLocal<?>> {// next:开放寻址法没有 next 指针// Key:与 WeakHashMap 相同,少了 key 的强引用// Hash:位于 ThreadLocal#threadLocalHashCode// Value:当前线程的副本Object value;Entry(ThreadLocal<?> k, Object v) {super(k/*注意:只有 Key 是弱引用*/);value = v;}}
}

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

其次,不管是 Key 还是 Value 使用弱引用都可以实现自动清理,至于使用哪一种方法各有优缺点,适用场景也不同。Key 弱引用的优点是外部不需要持有 Value 的强引用,缺点是存在 “重建 Key 不等价” 问题。由于 ThreadLocal 的应用场景是线程局部存储,我们没有重建多个 ThreadLocal 对象指向同一个键值对的需求,也没有重写 Object#equals() 方法,所以不存在重建 Key 的问题,使用 Key 弱引用更方便。

ThreadLocalMap 的获取方法

ThreadLocalMap 的获取方法相对简单,所以我们先分析,区分 2 种情况:

  1. 数组下标直接命中目标 Key,则直接返回,也不清理无效数据(这就是前文提到访问 ThreadLocal 不一定会触发清理的源码体现);
  2. 数组下标未命中目标 Key,则开始线性探测。探测过程中如果遇到 Key == null 的无效节点,则会调用 expungeStaleEntry() 清理连续段(说明即使触发清理,也不一定会扫描整个散列表)。

expungeStaleEntry() 是 ThreadLocalMap 核心的连续段清理方法,下文提到的 replaceStaleEntry() 和 cleanSomeSlots() 等清理方法都会直接或间接调用到 expungeStaleEntry()。 它的逻辑很简单:就是线性遍历从 staleSlot 位置开始的连续段:

  1. k == null 的无效节点: 清理;
  2. k ≠ null 的有效节点,再散列到新的位置上。
    在这里插入图片描述
    为什么要对有效节点再散列呢?
    线性探测只会遍历连续段,而清理无效节点会导致连续段产生断层。如果没有对有效节点做再散列,那么有效节点在下次查询时就有可能探测不到了。

ThreadLocalMap 的添加方法

ThreadLocalMap#set 的流程非常复杂,我将主要步骤概括为 6 步:

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

replaceStaleEntry(): 清理连续段中的无效节点的同时,如果目标节点存在则更新 Value 后替换到 staleSlot 无效节点位置,如果不存在则创建新节点替换到 staleSlot 无效节点位置。

cleanSomeSlots(): 对数式清理,清理复杂度比全数组清理低,在大多数情况只会扫描 log(len) 个元素。如果扫描过程中遇到无效节点,则从该位置执行一次连续段清理,再从连续段的下一个位置重新扫描 log(len) 个元素,直接结束对数扫描。
在这里插入图片描述

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 替换为新 Valuee.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;}
}

ThreadLocalMap 的扩容方法

ThreadLocalMap 的扩容方法相对于添加方法比较好理解。在添加方法中,如果添加键值对后散列值的长度超过扩容阈值,就会调用 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) {// 清除无效键值的 Valuee.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;}
}

ThreadLocalMap 的移除方法

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;}}}
}

ThreadLocalMap 复杂度分析

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

访问 ThreadLocal 一定会清理无效数据吗?

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

6. 总结

  1. ThreadLocal 是一种特殊的无锁线程安全方式,通过为每个线程分配独立的资源副本,从根本上避免发生资源冲突;
  2. ThreadLocal 在所有线程间隔离,InheritableThreadLocal 在创建子线程时会拷贝父线程中 InheritableThreadLocal 的有效键值对;
  3. 虽然 ThreadLocal 提供了自动清理数据的能力,但是自动清理存在滞后性。为了避免内存泄漏,在业务开发中应该及时调用 remove 清理无效的局部存储;
  4. ThreadLocal 是采用线性探测解决散列冲突的散列表。
    在这里插入图片描述

参考资料

数据结构与算法分析 · Java 语言描述(第 5 章 · 散列)—— [美] Mark Allen Weiss 著
算法导论(第 11 章 · 散列表)—— [美] Thomas H. Cormen 等 著
ThreadLocal 超强图解,这次终于懂了~

这篇关于多线程 | ThreadLocal源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1145177

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe

Java中最全最基础的IO流概述和简介案例分析

《Java中最全最基础的IO流概述和简介案例分析》JavaIO流用于程序与外部设备的数据交互,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),处理... 目录IO流简介IO是什么应用场景IO流的分类流的超类类型字节文件流应用简介核心API文件输出流应用文

java 恺撒加密/解密实现原理(附带源码)

《java恺撒加密/解密实现原理(附带源码)》本文介绍Java实现恺撒加密与解密,通过固定位移量对字母进行循环替换,保留大小写及非字母字符,由于其实现简单、易于理解,恺撒加密常被用作学习加密算法的入... 目录Java 恺撒加密/解密实现1. 项目背景与介绍2. 相关知识2.1 恺撒加密算法原理2.2 Ja

Nginx屏蔽服务器名称与版本信息方式(源码级修改)

《Nginx屏蔽服务器名称与版本信息方式(源码级修改)》本文详解如何通过源码修改Nginx1.25.4,移除Server响应头中的服务类型和版本信息,以增强安全性,需重新配置、编译、安装,升级时需重复... 目录一、背景与目的二、适用版本三、操作步骤修改源码文件四、后续操作提示五、注意事项六、总结一、背景与

Android实现图片浏览功能的示例详解(附带源码)

《Android实现图片浏览功能的示例详解(附带源码)》在许多应用中,都需要展示图片并支持用户进行浏览,本文主要为大家介绍了如何通过Android实现图片浏览功能,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码