当前位置: 首页 > news >正文

ThreadLocalMap

ThreadLocalMap

文章目录

  • ThreadLocalMap
    • 1. `ThreadLocal` 的基础结构
      • 1.1 类继承关系
      • 1.2 `ThreadLocalMap` 内部类概述
      • 1.3 弱引用键的设计理念
    • 2. `ThreadLocalMap`的实现机制
      • 2.1 Entry结构与弱引用
      • 2.2 哈希表设计
      • 2.3 解决哈希冲突的策略
    • 3. `ThreadLocalMap`的关键操作源码分析
      • 3.1 set方法实现
      • 3.2 get方法实现
      • 3.3 remove方法实现
      • 3.4 过期Entry的清理机制
    • 4. 核心操作源码分析
      • 4.1 `ThreadLocal`.set方法实现
      • 4.2 `ThreadLocal.get`方法实现
      • 4.3 `ThreadLocal.remove`方法实现
      • 4.4 `ThreadLocal.withInitial`方法
    • 5. 内存泄漏问题深度剖析
      • 5.1 为什么会发生内存泄漏
      • 5.3 源码中的防泄漏设计
      • 5.4 清理机制的触发时机

你是否曾遇到这样的场景:一个对象需要在同一个线程内的多个方法中传递,却不想把它作为参数在每个方法间传来传去?或者你需要保存一个"线程上下文"信息,比如用户身份、事务ID或请求追踪信息?

在这里插入图片描述

在并发的世界里,全局变量是危险的。正如一个热闹的宿舍里,把你的私人物品放在公共区域,总会有人"不小心"把它们挪走。ThreadLocal就像是给每个线程发了一个专属保险柜,只有线程自己能存取,线程间互不干扰。

然而,这个看似简单的工具类背后隐藏着巧妙的设计和潜在的风险。Java面试中,ThreadLocal的原理和内存泄漏问题"几乎成了高频标配题。许多开发者对ThreadLocal的印象仅限于"每个线程拥有独立的变量副本",但对其内存模型、实现机制和正确使用姿势知之甚少。

1. ThreadLocal 的基础结构

1.1 类继承关系

我们来看一下ThreadLocal的类定义:

public class ThreadLocal<T> {// 实现代码...
}

ThreadLocal是一个泛型类,类型参数T表示它可以存储任何类型的对象。相比其他并发工具类,ThreadLocal的类结构非常简单,它没有继承任何类,也没有实现任何接口。

这种设计反映了它的专注性——只做一件事:为每个线程提供独立的变量副本。

1.2 ThreadLocalMap 内部类概述

ThreadLocal最核心的部分是其内部私有静态类ThreadLocalMap

static class ThreadLocalMap {/*** Entry继承自WeakReference,key是ThreadLocal对象的弱引用*/static class Entry extends WeakReference<ThreadLocal<?>> {/** 与这个ThreadLocal关联的值 */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/** 初始容量 - 必须是2的幂 */private static final int INITIAL_CAPACITY = 16;/** 存放数据的table,大小必须是2的幂 */private Entry[] table;/** table中的entry数量 */private int size = 0;/** 扩容阈值,默认为0 */private int threshold = 0;// 其他字段和方法...
}

乍一看,这个ThreadLocalMap结构很像HashMap,都是基于哈希表实现的。但有几个关键区别:

  1. ThreadLocalMapThreadLocal的静态内部类,但它的实例是保存在Thread类中的

  2. Entry的key是ThreadLocal对象的弱引用,这对内存管理非常重要

  3. ThreadLocalMap没有实现Map接口,是一个定制的哈希表

1.3 弱引用键的设计理念

ThreadLocalMap中使用弱引用来保存ThreadLocal对象,这是一个精妙的设计:

static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);  // 将ThreadLocal对象作为弱引用value = v;}
}

为什么要用弱引用?想象这样一个场景:

void someMethod() {ThreadLocal<User> userThreadLocal = new ThreadLocal<>();userThreadLocal.set(new User("John"));// 使用userThreadLocal// 方法结束,userThreadLocal变量不再可访问
}

当方法执行完毕,局部变量userThreadLocal就会被销毁,但是Thread对象中的ThreadLocalMap仍然持有对这个ThreadLocal的引用。如果是强引用,那么即使userThreadLocal变量已经不可访问,ThreadLocal对象也不会被垃圾回收,这可能导致内存泄漏。

通过使用弱引用,一旦外部不再持有ThreadLocal的强引用,垃圾回收器就可以回收这个ThreadLocal对象,即使它仍被ThreadLocalMap引用。

但是,此时Entry中的value还是强引用,不会被自动回收,这就是常说的ThreadLocal内存泄漏的根源。后面我们会详细讨论这个问题。

2. ThreadLocalMap的实现机制

2.1 Entry结构与弱引用

ThreadLocalMap内部使用Entry数组来存储数据:

private Entry[] table;static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

每个Entry包含:

  • 一个ThreadLocal的弱引用作为key
  • 一个普通的强引用Object作为value

值得注意的是,WeakReference是JDK提供的一种特殊引用类型,当一个对象只剩下弱引用指向它时,垃圾回收器会在下一次GC时回收该对象。

// 弱引用的基本使用
ThreadLocal<?> threadLocal = new ThreadLocal<>();
WeakReference<ThreadLocal<?>> weakRef = new WeakReference<>(threadLocal);// 如果此时threadLocal = null,那么weakRef.get()在下一次GC后可能返回null
threadLocal = null;
System.gc();  // 触发GC
ThreadLocal<?> referent = weakRef.get();  // 可能已经是null

这种设计让ThreadLocal实例在不再需要时可以被回收,同时Entry中的value依然存在,直到Thread结束或者手动调用remove()方法。

2.2 哈希表设计

ThreadLocalMap使用开放地址法解决哈希冲突:

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn 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)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

HashMap使用链表或红黑树解决冲突不同,ThreadLocalMap使用线性探测法:如果计算出的槽位已被占用,就继续往后查找,直到找到空槽位或找到目标Entry。

ThreadLocalMap中每个ThreadLocal的哈希值通过一个特殊的原子递增生成器计算:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里使用的增量0x61c88647是一个神奇的数字,它是斐波那契散列乘数,能够使哈希值均匀分布,减少冲突。

2.3 解决哈希冲突的策略

当发生哈希冲突时,ThreadLocalMap采用线性探测法来解决:

private void set(ThreadLocal<?> key, Object value) {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)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

这段代码展示了ThreadLocalMap如何处理冲突:

  1. 计算初始槽位i

  2. 如果该位置已被占用,检查key是否相同

    1. 如果key相同,更新value并返回
    2. 如果key为null(已被GC回收),替换这个"陈旧"的Entry
    3. 否则,继续查找下一个位置
  3. 如果找到空槽位,插入新Entry

  4. 增加size并检查是否需要清理或扩容

值得注意的是,在查找过程中如果发现key为null的Entry(表示ThreadLocal已被回收),会主动清理这些Entry,这是一种"顺手清理"的机制,有助于减少内存泄漏。

3. ThreadLocalMap的关键操作源码分析

3.1 set方法实现

set方法是ThreadLocalMap最核心的方法之一,用于设置键值对:

private void set(ThreadLocal<?> key, Object value) {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)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

这段代码处理了三种情况:

  1. 找到相同的key,更新value

  2. 找到key为null的"陈旧"Entry,替换它

  3. 找到空槽位,插入新Entry

其中第2点很重要,这是ThreadLocalMap自动清理机制的一部分。当检测到key为null的Entry时,调用replaceStaleEntry方法替换并清理:

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// 向前扫描,查找更多的陈旧Entryint slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) {if (e.get() == null)slotToExpunge = i;}// 向后遍历for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果找到key,与staleSlot位置的Entry交换if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// 如果在前面的扫描中没有找到陈旧条目,则起点为这里if (slotToExpunge == staleSlot)slotToExpunge = i;// 清理陈旧的EntrycleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 如果我们还没有找到staleSlot之前的陈旧条目if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// 如果我们没有找到key,把新的Entry放入staleSlottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 如果有其他的陈旧Entry,清理它们if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

这个方法相当复杂,它不仅替换了陈旧的Entry,还会主动清理其他陈旧Entry,以减少内存泄漏的可能性。

3.2 get方法实现

get方法用于获取与ThreadLocal关联的值:

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn 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)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}

get方法首先计算哈希索引,然后检查该位置的Entry。如果key不匹配或为null,则调用getEntryAfterMiss继续查找。

同样,在查找过程中如果发现key为null的Entry,会调用expungeStaleEntry进行清理,这是另一个"顺手清理"的时机。

3.3 remove方法实现

remove方法用于移除ThreadLocal关联的值:

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);  // 清理这个槽位及后续陈旧的Entryreturn;}}
}

remove方法首先查找key对应的Entry,找到后调用Entry.clear()方法清除ThreadLocal的弱引用,然后调用expungeStaleEntry清理这个槽位及后续可能的陈旧Entry。

这个方法是彻底清除ThreadLocal数据的正确方式,它不仅移除了value的强引用,还处理了可能的连锁效应。

3.4 过期Entry的清理机制

ThreadLocalMap内部实现了多种清理机制来处理过期(陈旧)的Entry:

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清除指定槽位的Entrytab[staleSlot].value = null;tab[staleSlot] = null;size--;// 重新哈希后续的EntryEntry 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;// 如果需要,重新放置Entry到正确位置while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}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) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;
}private void rehash() {expungeStaleEntries();  // 完全清理所有陈旧Entry// 如果大小仍然超过阈值的3/4,则扩容if (size >= threshold - threshold / 4)resize();
}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)expungeStaleEntry(j);}
}

ThreadLocalMap提供了多个级别的清理机制:

  1. 定点清理:expungeStaleEntry方法清理指定位置的陈旧Entry,并重新哈希后续Entry

  2. 探测式清理:cleanSomeSlots方法在有限步数内查找并清理陈旧Entry

  3. 全量清理:expungeStaleEntries方法扫描整个表,清理所有陈旧Entry

这些清理机制在不同操作中被触发:

  • set操作可能触发探测式清理和rehash
  • get操作在查找过程中可能触发定点清理
  • remove操作总是触发定点清理
  • 当size达到阈值时,会触发全量清理和可能的扩容

通过这些多层次的清理机制,ThreadLocalMap尽量减少内存泄漏的可能性,但并不能完全避免。

4. 核心操作源码分析

4.1 ThreadLocal.set方法实现

ThreadLocal的set方法是我们直接使用的API,看看它是如何实现的:

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

这个方法非常简洁:

  1. 获取当前线程对象

  2. 尝试获取线程中的ThreadLocalMap

  3. 如果map存在,调用map.set设置值

  4. 如果map不存在,创建一个新的ThreadLocalMap

关键点在于:ThreadLocalMap实例是存储在Thread对象中的,而不是ThreadLocal对象中。每个Thread都有自己的ThreadLocalMap,用于存储所有与该线程关联的ThreadLocal变量。

这就是ThreadLocal能够实现线程隔离的核心机制——数据实际上是存储在线程内部的。

4.2 ThreadLocal.get方法实现

get方法用于获取当前线程下的变量值:

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}protected T initialValue() {return null;
}

get方法的逻辑是:

  1. 获取当前线程的ThreadLocalMap

  2. 如果map存在,尝试获取this作为key的Entry

  3. 如果Entry存在,返回其value

  4. 否则,调用setInitialValue设置并返回初始值

initialValue方法默认返回null,但可以被子类重写以提供自定义的初始值。这是一个很有用的特性,比如可以用来定义线程本地变量的默认值。

4.3 ThreadLocal.remove方法实现

remove方法用于移除当前线程中的变量:

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}

这个方法同样简单明了:获取当前线程的ThreadLocalMap,如果存在则从中移除this对应的Entry。

正确调用remove方法对防止内存泄漏至关重要,尤其是在线程池环境中。线程被重用时,如果不清理ThreadLocal变量,新的任务可能会访问到之前任务设置的值,既造成了内存泄漏,又可能导致逻辑错误。

4.4 ThreadLocal.withInitial方法

从Java 8开始,ThreadLocal增加了一个工厂方法withInitial:

    return new SuppliedThreadLocal<>(supplier);
}static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected T initialValue() {return supplier.get();}
}

这个方法创建一个特殊的ThreadLocal子类,它的initialValue方法使用提供的Supplier来获取初始值。这提供了一种更简洁的方式来创建带初始值的ThreadLocal

// 旧方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}
};// 新方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

这种函数式风格更加简洁,也更符合Java 8的编程习惯。

5. 内存泄漏问题深度剖析

5.1 为什么会发生内存泄漏

ThreadLocal的内存泄漏问题是众所周知的,让我们深入分析这个问题:

Thread实例 -> ThreadLocalMap -> Entry -> value-> Entry -> value-> Entry -> value (key为null)

ThreadLocal对象不再被引用时,Entry中的key变成null(因为是弱引用),但value仍然被强引用。如果线程长时间存活(如线程池中的线程),这些无法访问的value就会一直占用内存,形成泄漏。

具体来说,泄漏发生的条件是:

  1. ThreadLocal对象不再被外部引用

  2. 线程一直存活

  3. 没有手动调用remove方法清理

在这种情况下,虽然ThreadLocal对象会被回收,但其关联的value不会被回收,而且无法再通过正常手段访问到这些value。

5.2 弱引用与内存泄漏的关系

ThreadLocalMap中的Entry继承自WeakReference,key是ThreadLocal的弱引用:

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

弱引用的特性是:当对象只被弱引用引用时,垃圾回收器会在下一次回收中回收该对象。这意味着当ThreadLocal对象不再被外部强引用时,Entry中的key会变成null。

但这里有一个问题:虽然key会被回收,但value是强引用,不会自动回收。如果没有其他机制处理这些key为null的Entry,就会导致value无法被回收,形成内存泄漏。

ThreadLocalMap的设计者意识到了这个问题,所以在get、set、remove等操作中添加了清理机制,但这些清理是"顺手"进行的,不保证及时清理所有过期Entry。

5.3 源码中的防泄漏设计

ThreadLocalMap中包含多种防止内存泄漏的设计,我们前面已经分析过:

  1. 在set过程中清理:发现key为null的Entry时,会调用replaceStaleEntry或expungeStaleEntry进行清理

  2. 在get过程中清理:getEntryAfterMiss方法中,发现key为null时会调用expungeStaleEntry清理

  3. 在remove时清理:remove方法直接调用expungeStaleEntry清理当前和后续的过期Entry

  4. 在扩容前全量清理:rehash方法首先调用expungeStaleEntries清理所有过期Entry

​ 这些清理机制都是为了减少内存泄漏的可能性,但它们都有一个共同的问题:必须有人调用ThreadLocalMap的方法才会触发清理。如果一个线程不再使用任何ThreadLocal,但线程本身还在运行,那么这些过期的Entry就不会被自动清理。

5.4 清理机制的触发时机

让我们总结一下ThreadLocalMap中清理机制的触发时机:

  1. set时触发:向ThreadLocalMap中设置值时,可能触发清理

  2. get时触发:获取值时,如果发生哈希冲突,可能触发清理

  3. remove时触发:移除值时,必定触发清理

  4. resize时触发:扩容前,会进行全量清理

这些清理机制虽然可以减少内存泄漏的可能性,但都不能完全避免泄漏。最安全的做法仍然是在不再需要ThreadLocal变量时,显式调用remove方法进行清理。

特别是在线程池环境中,线程结束任务后会被重用,如果不清理ThreadLocal,可能导致数据污染和内存泄漏。因此,ThreadLocal变量的生命周期应该与任务的生命周期一致,而不是与线程的生命周期一致。

http://www.xdnf.cn/news/372907.html

相关文章:

  • 自主shell命令行解释器
  • STM32f103 标准库 零基础学习之点灯
  • 初等数论--莫比乌斯反演
  • spark-Join Key 的基数/rand函数
  • 设计模式【cpp实现版本】
  • 从前端视角看网络协议的演进
  • 从 SpringBoot 到微服务架构:Java 后端开发的高效转型之路
  • 访问者模式(Visitor Pattern)详解
  • FPGA笔试题review
  • 【Linux系列】跨平台安装与配置 Vim 文本编辑器
  • 开疆智能Canopen转Profinet网关连接工博士GBS20机器人配置案例
  • redis八股--1
  • HunyuanCustom:文生视频框架论文速读
  • 2025盘古石初赛WP
  • Anaconda的简单使用
  • 垃圾对象回收
  • 从杰夫・托尔纳看 BPLG 公司的技术创新与发展
  • 学习黑客5 分钟深入浅出理解Linux Packages Software Repos
  • vue 中的ref
  • Java大师成长计划之第17天:锁与原子操作
  • 深入浅出 JDBC 与数据库连接池
  • 嵌入式开发学习(阶段二 C语言基础)
  • Java 24新特性深度解析:从优化技巧到高手进阶指南
  • PyQt5基本窗口控件(QWidget)
  • 嵌入式STM32学习——继电器
  • 数据分析-图2-图像对象设置参数与子图
  • 深入浅出之STL源码分析3_类模版实例化与特化
  • 【Java ee初阶】网络原理
  • Spring Boot 中如何启用 MongoDB 事务
  • 教育系统源码如何支持白板直播与刷题功能?功能开发与优化探索