ThreadLocal 线程变量

124 阅读9分钟

ThreadLocal 线程变量

ThreadLocal 是 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 方法

    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
    }
    ThreadLocalMap getMap(Thread t) {  
        return t.threadLocals;    //     ThreadLocal.ThreadLocalMap threadLocals = null;
    }

流程:

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

ThreadLocal 的 get 方法

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

流程:

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

ThreadLocal 的 remove 方法

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

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

remove() 方法实现

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

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

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

流程:

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”。

代码实现示例:

//定义 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 类实现,当一个对象仅被弱引用指向时,无论内存是否存在,该对象都会在下一次垃圾回收时被回收。

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

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

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

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

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
流程:
// 初始状态
[主线程]
  ↳ 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),用来存放这本笔记本和其他私人物品