ThreadLocal 线程变量

ThreadLocal 线程变量

ThreadLocal 是 Java 中用于实现线程局部变量的工具类,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

共享实例 线程隔离:

java 复制代码
/**
     * threadLocal 线程变量
     * 多线程使用的是同一个实例的 threadLocal 但是每个线程的 threadLocal 是独立的
     * 每个线程又有自己的线程副本
     *
     * 共享实例,线程隔离
     */
   private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

   public static void print(String str){  //参数:线程名字
       System.out.println("str: " + threadLocal.get());
       threadLocal.remove();
   }

   @Test
   public  void testThreadLocal() throws InterruptedException {
       new Thread(new Runnable() {
           @Override
           public void run() {
               threadLocal.set("thread1");
               print("thread1");
               System.out.println("表明同一个threadLocal 实例 = " + threadLocal);
               System.out.println("删除线程变量后:threadLocal1 = " + threadLocal.get());
           }
       }).start();

       Thread.sleep(1000);

       new Thread(new Runnable() {
           @Override
           public void run() {
               threadLocal.set("thread2");
               print("thread2");
               System.out.println("表明同一个threadLocal 实例 " + threadLocal);
               System.out.println("删除线程变量后:threadLocal2 = " + threadLocal.get());
           }
       }).start();
   }

ThreadLocal 的 set 方法

java 复制代码
    public void set(T value) {
        Thread t = Thread.currentThread();  //获取当前线程
        ThreadLocalMap map = getMap(t);
        if (map != null)   //判断当前线程中的 ThreadLocalMap属性 是否为空
            map.set(this, value); //如果 map 存在,以当前 ThreadLocal 实例为键,存储 value
        //这里的 this 指的是当前 ThreadLocal 实例,它作为键存储在 ThreadLocalMap 中。
        else
            createMap(t, value);//如果 map 不存在,创建一个新的 ThreadLocalMap
    }
java 复制代码
    ThreadLocalMap getMap(Thread t) {  
        return t.threadLocals;    //     ThreadLocal.ThreadLocalMap threadLocals = null;
    }

流程:

kotlin 复制代码
set(value)
  ├── 获取当前线程 t
  ├── 获取 t 的 threadLocals
  │     ├── 若存在 → 调用 map.set(this, value)
  │     │         ├── 计算索引 i
  │     │         ├── 遍历数组
  │     │         │     ├── 找到键 → 更新值
  │     │         │     ├── 找到 stale 槽 → 替换并清理
  │     │         │     └── 未找到 → 创建新 Entry
  │     │         └── 检查是否需要扩容
  │     └── 若不存在 → 创建新的 ThreadLocalMap
  └── 结束

ThreadLocal 的 get 方法

java 复制代码
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的 ThreadLcoalMap
        ThreadLocalMap map = getMap(t);
        // 3.判断map是否存在
        if (map != null) {
            //以当前 ThreadLocal 实例为键,查找 Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
java 复制代码
    private T setInitialValue() {
        T value = initialValue();  //null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

流程:

kotlin 复制代码
get()
  ├── 获取当前线程 t
  ├── 获取 t 的 threadLocals
  │     ├── 若存在 → 调用 map.getEntry(this)
  │     │         ├── 计算索引 i
  │     │         ├── 检查表[i]
  │     │         │     ├── 找到键 → 返回值
  │     │         │     ├── 键为 null → 清理并继续查找
  │     │         │     └── 未找到 → 继续线性探测
  │     │         └── 未找到键 → 返回 null
  │     └── 若不存在 → 创建新的 ThreadLocalMap 并设置初始值
  └── 返回值(或初始值)

ThreadLocal 的 remove 方法

ThreadLocalremove() 方法用于从当前线程的 ThreadLocalMap 中移除该 ThreadLocal 实例对应的值。这是避免内存泄漏的关键操作

java 复制代码
    public void remove() {
        //同样获取当前线程的 ,然后获取当前线程的 ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove() 方法实现

java 复制代码
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 计算索引
    int i = key.threadLocalHashCode & (len-1);
    
    // 2. 线性探测查找目标键
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        
        if (e.get() == key) {
            // 3. 找到目标键后,调用 WeakReference 的 clear() 方法
            e.clear();
            
            // 4. 清理过期条目(防止内存泄漏)
            expungeStaleEntry(i);
            return;
        }
    }
}

为了防止内存泄露,我们必须要remove

scss 复制代码
在 ThreadLocal 的生命周期中,如果没有手动调用 remove(),可能会导致以下问题:

- 当 ThreadLocal 实例的外部强引用被回收后,其在 ThreadLocalMap 中的弱引用键会变为 null。
- 但如果线程(如线程池中的线程)长期存活,ThreadLocalMap 中的值可能不会被回收,造成内存泄漏。
- 调用 remove() 可以主动清理这些过期条目,避免内存泄漏。

流程:

scss 复制代码
remove()
  ├── 获取当前线程 t
  ├── 获取 t 的 threadLocals
  │     ├── 若存在 → 调用 map.remove(this)
  │     │         ├── 计算索引 i
  │     │         ├── 线性探测查找键
  │     │         │     ├── 找到键 → 调用 e.clear()
  │     │         │     └── 调用 expungeStaleEntry(i) 清理
  │     └── 若不存在 → 无操作
  └── 结束

ThreadLocal 的 实战使用

快递分拣中心的编号标签管理

场景描述:

你是一个在快递分拣中心工作的工作人员,每个分拣员(线程)负责将快递包裹(任务)按目的地分类,为了方便追踪,每个包裹需要粘贴一个 唯一编号标签 (如:"分拣员1-001","分拣员2-001")。

如果没有 ThreadLocal ,所有分拣员共用一个编号本的话,就有可能造成编号混乱的情况出现,例如:

  • 分拣员1:给包裹贴了"分拣员1-001",但是编号本被分拣员2误操作,导致编号重复或跳号。

  • 分拣员2:在分拣时,不小心使用了分拣员1 的编号,造成数据错误。

1. 问题:共享资源导致的编号混乱
  • 如果没有 ThreadLocal,所有分拣员共用一个编号生成器(如 AtomicInteger),会出现以下问题:

    • 线程安全问题:多个线程同时修改共享变量,可能导致数据不一致。
    • 编号冲突:分拣员无法快速生成专属编号,效率低下。

2. 解决方案:用 ThreadLocal 为每个线程分配独立的编号本
  • 通过 ThreadLocal,每个线程(分拣员)都有自己的 AtomicInteger(编号本),独立生成编号。这样:

    • 分拣员1 的编号本只用于自己,生成"分拣员1-001"、"分拣员1-002"。

代码实现示例:

java 复制代码
//定义 ThreadLocal ,为每个线程分配独立的编号本
// 定义 ThreadLocal,为每个线程分配独立的编号本(AtomicInteger)
public class SortingContext {
    private static final ThreadLocal<AtomicInteger> counter = ThreadLocal.withInitial(() -> new AtomicInteger(1));

    // 获取当前线程的编号本,并生成下一个编号
    public static String generateLabel() {
        return Thread.currentThread().getName() + "-" + counter.getAndIncrement();
    }

    // 清理资源(避免内存泄漏)
    public static void clear() {
        counter.remove();
    }
}

// 模拟分拣员(线程)处理包裹
public class Sorter implements Runnable {
    @Override
    public void run() {
        try {
            // 1. 分拣员开始分拣包裹,生成编号
            for (int i = 0; i < 3; i++) {
                String label = SortingContext.generateLabel();
                System.out.println(label + " 的包裹已贴上标签");
            }
        } finally {
            // 2. 分拣结束后,清理编号本(避免内存泄漏)
            SortingContext.clear();
        }
    }
}

// 启动多个分拣员线程
public class SortingCenter {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            Thread sorter = new Thread(new Sorter(), "分拣员-" + i);
            sorter.start();
        }
    }
}

扩展:

1️⃣:如何理解 当 ThreadLocal 实例的外部强引用被回收后,其在 ThreadLocalMap 中的弱引用键会变为 null。

在Java 中,对象的引用分为不同的引用类型,其中最常见的是强引用和弱引用

  • 强引用

    最常见的引用类型,例如: Object obi = new Object(); 只要强引用存在,对象就不会被垃圾回收。

  • 弱引用

    通过 WeakReference 类实现,当一个对象仅被弱引用指向时,无论内存是否存在,该对象都会在下一次垃圾回收时被回收。

    java 复制代码
    Object strongRef = new Object(); // 强引用
    WeakReference<Object> weakRef = new WeakReference<>(strongRef); // 弱引用指向同一个对象
    
    strongRef = null; // 断开强引用
    // 此时对象仅被 weakRef 弱引用指向,下一次 GC 时会被回收

ThreadLocal 的数据存储在每个线程的 ThreadLocalMap 中,而 ThreadLocalMap 的键是 WeakReference<ThreadLocal<?>>:

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // 弱引用指向 ThreadLocal 实例
        value = v;
    }
}

ThreadLocalMap 的键是对 ThreadLocal 实例的弱引用,而值是用户存储的对象(强引用)。

java 复制代码
public class MyClass {
    // 静态变量是强引用,指向 ThreadLocal 实例
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        threadLocal.set(123); // 存储值到当前线程的 ThreadLocalMap
        
        // 假设后续代码中,断开了对 threadLocal 的强引用
        threadLocal = null;
    }
}

此时内存中的状态:

  • 外部强引用消失:threadLocal = null 后,没有任何强引用指向 ThreadLocal 实例。
  • 弱引用仍然存在:ThreadLocalMap 的键(Entry)通过弱引用指向 ThreadLocal 实例。

当垃圾回收发生时:

  • ThreadLocal 实例被回收:由于没有强引用,仅存的弱引用无法阻止 GC 回收该实例。
  • 弱引用键变为 null:ThreadLocal 实例被回收后,Entry 中的弱引用 get() 方法返回 null
流程:
ini 复制代码
// 初始状态
[主线程]
  ↳ static threadLocal → ThreadLocal@1234  ← 弱引用  [ThreadLocalMap 的 Entry]
                                             ↳ value = 123

// 执行 threadLocal = null 后
[主线程]
  ↳ static threadLocal = null
  
[ThreadLocalMap 的 Entry]
  ↳ 弱引用 → null (ThreadLocal@1234 已被 GC 回收)
  ↳ value = 123 (强引用仍然存在)

弱引用管键(ThreadLocal 实例),强引用管值(用户数据)

正是因为,我们的key :threadLocal 是弱引用,在key 被垃圾回收之后,就会出现key 为 null 的value ,如果不清理,就会造成内存泄露。所以我们要及时 remove 。

2️⃣: ThreadThreadLocalThreadLocalMap 三者之间的关系

Thread : (线程)

  • 线程是操作系统能够进行运算调度的最小单位,进程包括线程。一个进程包括多个线程。

ThreadLocal : (线程局部变量)

  • 为每个线程提供一个独立的变量副本,每个线程拥有可以独立改变自己的副本,而不会影响其他线程的变量副本。

ThreadLocalMap : (线程局部变量映射表)

  • ThreadLocalMap 是 ThreadLocal 的静态内部类,每个 Thread 对象都包含一个 ThreadLocalMap 成员变量(threadLocals)。它用于存储线程的局部变量,键为 ThreadLocal 实例,值为用户存储的对象。弱引用键 :键(ThreadLocal 实例)是弱引用,值是强引用。

关系:

每个线程都有自己的 一个 ThreadLocalMap(哈希表) 属性,键为 ThreadLocal 对象实例,值为线程对应的变量副本的值。一个线程 可以绑定多个 ThreadLocal 实例,每个 ThreadLocal 都会作为 ThreadLocalMap 的键(Key),与对应的值(Value)一起存储在同一个 ThreadLocalMap 中。每个线程(Thread)内部维护一个 ThreadLocalMap 对象(threadLocals 字段),用于存储该线程的所有 ThreadLocal 变量及其对应的值。

角色 核心功能 隔离性 生命周期
Thread 执行单元 每个线程独立 线程创建到销毁
ThreadLocal 提供线程局部变量的访问接口 线程间数据隔离 由外部引用控制
ThreadLocalMap 存储线程局部变量 每个线程独立一份 随线程生命周期变化

例子:

  • 假设你在一家公司上班,每个**员工(线程)**每天需要记录自己的工作任务。
  • 公司规定:每人必须有一本独立的笔记本(不能共享),用来记录自己的待办事项。
  • 这本笔记本(ThreadLocal)有个特性:只有你自己能打开并修改里面的内容,其他同事看不到也改不了
  • 而公司会给每个员工分配一个专属的文件柜(ThreadLocalMap) ,用来存放这本笔记本和其他私人物品
相关推荐
sky_ph2 分钟前
JAVA-GC浅析(二)G1(Garbage First)回收器
java·后端
涡能增压发动积7 分钟前
一起来学 Langgraph [第二节]
后端
IDRSolutions_CN24 分钟前
PDF 转 HTML5 —— HTML5 填充图形不支持 Even-Odd 奇偶规则?(第二部分)
java·经验分享·pdf·软件工程·团队开发
hello早上好27 分钟前
Spring不同类型的ApplicationContext的创建方式
java·后端·架构
roman_日积跬步-终至千里27 分钟前
【Go语言基础【20】】Go的包与工程
开发语言·后端·golang
HelloWord~1 小时前
SpringSecurity+vue通用权限系统2
java·vue.js
让我上个超影吧1 小时前
黑马点评【基于redis实现共享session登录】
java·redis
00后程序员2 小时前
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
后端
HyggeBest2 小时前
Mysql的数据存储结构
后端·架构
TobyMint2 小时前
golang 实现雪花算法
后端