多线程 | 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 超强图解,这次终于懂了~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浙大数据结构:02-线性结构3 Reversing Linked List

数据结构MOOC PTA习题 这道题也是相当费事&#xff0c;不过比上一个题好一些&#xff0c;这里我使用了C的STL库&#xff0c;使得代码量大幅减少。 题干机翻&#xff1a; 1、条件准备 这里我准备采用map来存地址和值&#xff0c;因为map的查找效率也是不错的 数组arr是存链…

GPU环境配置:1.CUDA、Anaconda、Pytorch

一、查看显卡适配CUDA型号 查看自己电脑的显卡版本&#xff1a; 在 Windows 设置中查看显卡型号&#xff1a;使用 Windows I 快捷键打开「设置」&#xff0c;依次点击「系统」-「屏幕」和「高级显示器设置」&#xff0c;在「显示器 1」旁边就可以看到显卡名称。 右键点菜单图标…

43. 1 ~ n 整数中 1 出现的次数【难】

comments: true difficulty: 中等 edit_url: https://github.com/doocs/leetcode/edit/main/lcof/%E9%9D%A2%E8%AF%95%E9%A2%9843.%201%EF%BD%9En%E6%95%B4%E6%95%B0%E4%B8%AD1%E5%87%BA%E7%8E%B0%E7%9A%84%E6%AC%A1%E6%95%B0/README.md 面试题 43. 1 &#xff5e; n 整数中 1 …

前端 Vue3 项目开发—— ESLint prettier 配置代码风格

ESLint & prettier 介绍 如果你用的是 pnpm create vue 来创建项目&#xff0c;那么创建项目时就会让你选择是否添加 ESLint 和 prettier 我们在上一篇博客中详细介绍过 ESLint&#xff0c;可以说上一篇博客是这篇博客的先修知识&#xff0c;所以各位小伙伴们请先去看看我…

LiveQing视频点播流媒体RTMP推流服务功能-支持大疆等无人机RTMP推流支持OBS推流一步一步搭建RTMP视频流媒体服务示例

LiveQing支持大疆等无人机RTMP推流支持OBS推流一步一步搭建RTMP视频流媒体服务示例 1、流媒体服务搭建2、推流工具准备3、创建鉴权直播间4、获取推流地址5、配置OBS推流6、推流及播放7、获取播放地址7.1 页面查看视频源地址7.2 接口查询 8、相关问题8.1、大疆无人机推流花屏 9、…

【每日刷题】Day111

【每日刷题】Day111 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. LCR 047. 二叉树剪枝 - 力扣&#xff08;LeetCode&#xff09; 2. LCR 049. 求根节点到叶节点数字…

怎么在mathtype中打空格 MathType空格键不能用

MathType是一款数学公式编辑器&#xff0c;可以帮助用户创建复杂的数学公式和方程式。它提供了一个用户友好的界面&#xff0c;使得编辑和排版数学公式变得更加容易和高效。用户可以直接在其界面中输入公式&#xff0c;也可以将已有的公式从其他文档中复制粘贴过来进行编辑。在…

【STM32CubeMX】MPU6050移植DMP流程

原本是想要自己的模拟I2C库&#xff0c;来组合时选块&#xff0c;对接上DMP所需接口&#xff0c;可是一直卡在初始化&#xff0c;后面改成STM32F4的硬件I2C&#xff0c;也是很便捷的对接上接口了。此外在也参考了网上的移植资料与记录。本文也作为学习笔记&#xff0c;记录下过…

Java项目: 基于SpringBoot+mybatis+maven+mysql教师工作量管理系统(含源码+数据库+毕业论文)

一、项目简介 本项目是一套基于SpringBootmybatismavenmysql教师工作量管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观…

软件测试 - 性能测试 (概念)(并发数、吞吐量、响应时间、TPS、QPS、基准测试、并发测试、负载测试、压力测试、稳定性测试)

一、性能测试 目标&#xff1a;能够对个人编写的项目进行接口的性能测试。 一般是功能测试完成之后&#xff0c;最后做性能测试。性能测试是一个很大的范围&#xff0c;在学习过程中很难直观感受到性能。 以购物软件为例&#xff1a; 1&#xff09;购物过程中⻚⾯突然⽆法打开…

JRebel and XRebel离线安装

近期&#xff0c;使用JRebel and XRebel&#xff0c;发现总是安装不上&#xff0c;可能是网络的原因吧。所以就使用离线方式进行安装。 JRebel 是一款用于 Java 开发的生产力工具。它的主要功能是加速开发周期&#xff0c;通过在不重启 JVM 的情况下即时加载代码变更。这样&…

在VB.net中,如何把20240906转化成日期格式

标题 vb.net中&#xff0c;如何把20240906转化成日期格式 正文 在 VB.NET 中&#xff0c;将一个数字字符串&#xff08;如 "20240906"&#xff09;转换为日期格式&#xff0c;你可以使用 DateTime.Parse 或 DateTime.TryParse 方法。这些方法可以将符合日期格式的字符…

Github 2024-09-07Rust开源项目日报Top10

根据Github Trendings的统计,今日(2024-09-07统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Rust项目10CUE项目1Python项目1Go项目1Polars: Rust中的DataFrame接口和OLAP查询引擎 创建周期:1354 天开发语言:Rust, Python协议类型:MIT …

【STM32开发】GPIO最全解析及应用实例

目录 【1】GPIO概述 GPIO的基本概念 GPIO的应用 【2】GPIO功能描述 1.IO功能框图 2.知识补充 3.功能详述 浮空输入 上拉输入 下拉输入 模拟输入 推挽输出 开漏输出 复用开漏输出和复用推挽输出 【3】GPIO常用寄存器 相关寄存器介绍 4个32位配置寄存器 2个32位数据寄存器 1个32位…

机器学习如何用于音频分析?

机器学习如何用于音频分析&#xff1f; 一、说明 近十年来&#xff0c;机器学习越来越受欢迎。事实上&#xff0c;它被用于医疗保健、农业和制造业等众多行业。随着技术和计算能力的进步&#xff0c;机器学习有很多潜在的应用正在被创造出来。由于数据以多种格式大量可用&…

JVM系列(十) -垃圾收集器介绍

一、摘要 在之前的几篇文章中,我们介绍了 JVM 内部布局、对象的创建过程、运行期的相关优化手段以及垃圾对象的回收算法等相关知识。 今天通过这篇文章,结合之前的知识,我们一起来了解一下 JVM 中的垃圾收集器。 二、垃圾收集器 如果说收集算法是内存回收的方法论,那么…

OrangePi AIpro 香橙派 昇腾 Ascend C 算子开发 与 调用 - 通过aclnn调用的方式调用AddCustom算子

OrangePi AIpro 香橙派 昇腾 Ascend C 算子开发 与 调用 通过aclnn调用的方式调用 - AddCustom算子 - 单算子API执行(aclnn) 多种算子调用方式 *开发时间使用场景调用方式运行硬件基于Kernel直调工程&#xff08;快速&#xff09;少单算子调用&#xff0c;快速验证算法逻辑IC…

Kafka【十二】消费者拉取主题分区的分配策略

【1】消费者组、leader和follower 消费者想要拉取主题分区的数据&#xff0c;首先必须要加入到一个组中。 但是一个组中有多个消费者的话&#xff0c;那么每一个消费者该如何消费呢&#xff0c;是不是像图中一样的消费策略呢&#xff1f;如果是的话&#xff0c;那假设消费者组…

C语言-程序环境 #预处理 #编译 #汇编 #链接 #执行环境

文章目录 前言 一、程序的环境翻译和执行环境 二、翻译环境 (一)、整体把握 (一)、编译 1、预处理(预编译) 2、编译 a、词法分析 b、语法分析 c、语义分析 d、符号汇总 3、汇编 (二)、链接 三、运行环境 总结​​​​​​​ 前言 路漫漫其修远兮&#xff0c;吾将…

9月7日微语报,星期六,农历八月初五

&#xff19;月&#xff17;日微语报&#xff0c;星期六&#xff0c;农历八月初五&#xff0c;周末愉快&#xff01; 一份微语报&#xff0c;众览天下事&#xff01; 1、21个部门&#xff1a;符合条件的流动儿童家庭或可配公租房。 2、多所高校2025年招生简章显示&#xff0…