程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java常用开发工具包

    • Jackson(JSON处理库)
    • FastJson2(JSON处理库)
    • Gson(JSON处理库)
    • BeanUtils(对象复制工具)
    • MapStruct(对象转换工具)
    • Guava(开发工具包)
    • ThreadLocal(本地线程变量)
      • 1. ThreadLocal 概述与核心思想
        • 1.1 什么是 ThreadLocal?
        • 1.2 背景:多线程下的共享变量问题
        • 1.3 ThreadLocal vs. 同步锁 (Synchronized / Lock)
      • 2. ThreadLocal 的核心 API 与工作原理
        • 2.1 基本使用示例
        • 2.2 ThreadLocal 核心 API 详解
        • 2.3 ThreadLocal 工作原理解析
        • 2.3.1 set(T value) 方法原理
        • 2.3.2 get() 方法原理
        • 2.3.3 remove() 方法原理
        • 2.4 ThreadLocalMap 内部结构简述
        • 2.5 设计演进:为什么是 Thread 持有 Map?
      • 3. ThreadLocal 内存泄漏问题深度剖析
        • 3.1 内存泄漏的原因
        • 3.2 为什么设计成弱引用 Key?
        • 3.3 防止内存泄漏的最佳实践:remove()
      • 4. ThreadLocal 的典型应用场景
        • 4.1 场景一:管理每个请求的上下文信息(隐式参数传递)
        • 4.2 场景二:存储用户身份认证信息
        • 4.3 场景三:解决非线程安全的类在并发环境下的使用问题
      • 5. InheritableThreadLocal:让子线程继承父线程的值
        • 5.1 InheritableThreadLocal 介绍
        • 5.2 ThreadLocal vs InheritableThreadLocal 对比
        • ThreadLocal 在父子线程中的表现
        • InheritableThreadLocal 在父子线程中的表现
        • 5.3 InheritableThreadLocal 工作原理
        • 5.4 InheritableThreadLocal 在线程池中的局限性
      • 6. TransmittableThreadLocal (TTL):线程池上下文传递的终极方案
        • 6.1 TTL 介绍
        • 6.2 TTL 工作原理
        • 6.3 TTL 基本使用
        • 6.4 TTL 进阶:装饰线程池与自定义传递
      • 7. ThreadLocal 使用总结与建议
    • SLF4j(日志框架)
    • Lombok (注解插件)
  • 开发工具包
  • Java常用开发工具包
scholar
2024-02-16
目录

ThreadLocal(本地线程变量)

# ThreadLocal(本地线程变量)

前言

在并发编程领域,确保线程安全是核心挑战之一。ThreadLocal 作为 Java 并发包 (java.lang) 提供的独特机制,为解决多线程环境下共享变量的访问冲突提供了一种巧妙的思路。它并非通过加锁等同步手段来保证共享,而是为每个使用该变量的线程创建一个独立的副本,使得每个线程都能独立地修改自己的副本,而不会影响其他线程,从而天然地避免了并发问题。本文将深入探讨 ThreadLocal 的核心概念、工作原理、典型应用场景,并着重分析其潜在的内存泄漏风险及规避策略,同时介绍其扩展 InheritableThreadLocal 和在线程池场景下的利器 TransmittableThreadLocal。

# 1. ThreadLocal 概述与核心思想

# 1.1 什么是 ThreadLocal?

ThreadLocal,顾名思义,即“线程本地变量”。它提供了一种变量作用域的扩展,超越了传统的方法作用域、实例作用域和类作用域,创建了一种“线程作用域”。当你创建一个 ThreadLocal 变量时,访问这个变量的每个线程都会拥有该变量的一个本地、独立的初始值副本。线程对这个副本的任何修改都只影响当前线程,对其他线程是不可见的。

这种机制的本质是数据隔离。它将共享数据的并发访问问题,转化为每个线程访问自己私有数据的模式,从而避免了同步。

# 1.2 背景:多线程下的共享变量问题

在多线程环境中,多个线程可能会同时读写同一个共享变量。这些共享变量通常存储在:

  • 堆内存 (Heap):实例对象的成员变量、静态变量、数组元素等。

Java 内存模型 (JMM) 定义了线程如何与主内存以及它们各自的工作内存交互。当多个线程操作共享变量时,若没有适当的同步措施,就可能出现数据竞争、可见性问题、指令重排序等导致的线程安全问题。

Java内存模型示意图

# 1.3 ThreadLocal vs. 同步锁 (Synchronized / Lock)

ThreadLocal 和传统的同步锁机制(如 synchronized 关键字或 java.util.concurrent.locks.Lock 接口)是解决线程安全问题的两种不同哲学:

特性 同步锁 (Synchronized / Lock) ThreadLocal
核心思想 时间换空间:只保留一份共享变量,通过锁机制让线程排队访问,牺牲并发性来保证数据一致性。 空间换时间:为每个线程创建变量副本,用额外的内存空间换取线程间的无锁并发访问。
关注点 保证多个线程对共享资源访问的同步性和互斥性。 实现多个线程对状态的隔离性,避免共享。
适用场景 多个线程需要协作完成对同一份数据的修改或访问。 每个线程需要维护自身独立的状态或上下文信息。

ThreadLocal 并不解决多个线程需要访问同一个共享实例的问题,而是为每个线程提供了一个独立的、隔离的存储空间。

# 2. ThreadLocal 的核心 API 与工作原理

# 2.1 基本使用示例

ThreadLocal 的使用通常遵循以下模式:

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 模拟用户实体类
 */
class User {
    private String name;
    // ... 其他属性和方法 ...
    public User(String name) { this.name = name; }
    @Override public String toString() { return "User{name='" + name + "'}"; }
}

/**
 * ThreadLocal 基本使用演示
 */
public class ThreadLocalBasicDemo {

    // 1. 声明 ThreadLocal 变量 (通常用 private static final 修饰)
    //    泛型 <User> 指定了 ThreadLocal 存储的数据类型
    private static final ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

    // 也可以使用 withInitial 提供初始值工厂
    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> {
                System.out.println("Initializing SimpleDateFormat for thread: " + Thread.currentThread().getName());
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            });


    /**
     * 向当前线程的 ThreadLocal 中存储数据
     * @param user 要存储的用户对象
     */
    public void setUserForCurrentThread(User user) {
        System.out.println(Thread.currentThread().getName() + " setting user: " + user);
        // 调用 set 方法,将 user 对象与当前线程关联起来
        threadLocalUser.set(user);
    }

    /**
     * 从当前线程的 ThreadLocal 中获取数据
     * @return 当前线程关联的用户对象,如果未设置过则返回 null (除非使用 withInitial)
     */
    public User getUserForCurrentThread() {
        // 调用 get 方法,获取与当前线程关联的 User 对象
        User user = threadLocalUser.get();
        System.out.println(Thread.currentThread().getName() + " getting user: " + user);
        return user;
    }

    /**
     * 清除当前线程在 ThreadLocal 中存储的数据
     * !!! 非常重要:防止内存泄漏,尤其在线程池场景下必须调用 !!!
     */
    public void clearUserForCurrentThread() {
        System.out.println(Thread.currentThread().getName() + " removing user...");
        // 调用 remove 方法,断开当前线程与存储值的关联
        threadLocalUser.remove();
    }

    /**
     * 使用带初始值的 ThreadLocal
     */
    public String getCurrentFormattedDate() {
        // 第一次调用 get 时,如果当前线程没有值,会调用 withInitial 提供的 Supplier 创建初始值
        SimpleDateFormat sdf = dateFormatThreadLocal.get();
        return sdf.format(new Date());
    }


    public static void main(String[] args) {
        ThreadLocalBasicDemo demo = new ThreadLocalBasicDemo();

        // 创建两个线程来演示线程隔离性
        Thread thread1 = new Thread(() -> {
            demo.setUserForCurrentThread(new User("Alice")); // Thread-0 设置 User
            demo.getUserForCurrentThread(); // Thread-0 获取自己的 User
            System.out.println("Thread-0 Date: " + demo.getCurrentFormattedDate()); // Thread-0 获取日期格式化器
            demo.clearUserForCurrentThread(); // Thread-0 清理
            demo.getUserForCurrentThread(); // 清理后再获取为 null
        }, "Thread-0");

        Thread thread2 = new Thread(() -> {
            try { Thread.sleep(50); } catch (InterruptedException e) {} // 确保 Thread-0 先执行一部分
            demo.setUserForCurrentThread(new User("Bob"));   // Thread-1 设置自己的 User
            demo.getUserForCurrentThread(); // Thread-1 获取自己的 User,与 Thread-0 不同
             System.out.println("Thread-1 Date: " + demo.getCurrentFormattedDate()); // Thread-1 获取自己的日期格式化器
            demo.clearUserForCurrentThread(); // Thread-1 清理
        }, "Thread-1");

        thread1.start();
        thread2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 2.2 ThreadLocal 核心 API 详解

ThreadLocal 类本身非常简单,主要提供以下三个公共方法:

  1. public void set(T value):

    • 作用: 将指定的值 value 与当前正在执行的线程关联起来。这个值存储在当前线程内部的一个特殊 Map 中。
    • 行为: 如果当前线程之前没有为此 ThreadLocal 对象设置过值,则创建关联;如果设置过,则覆盖旧值。
  2. public T get():

    • 作用: 获取当前正在执行的线程与此 ThreadLocal 对象关联的值。
    • 行为:
      • 如果当前线程之前调用过 set() 设置了值,则返回该值。
      • 如果当前线程从未调用过 set(),并且该 ThreadLocal 是通过 ThreadLocal.withInitial(Supplier) 创建的,则会调用 Supplier 的 get() 方法生成一个初始值,将该初始值与当前线程关联,并返回。
      • 如果当前线程从未调用过 set(),且没有提供 withInitial,则返回 null。
  3. public void remove():

    • 作用: 移除当前正在执行的线程与此 ThreadLocal 对象的关联。
    • 行为: 清除当前线程内部 Map 中关于此 ThreadLocal 的条目。这是防止内存泄漏的关键操作。调用后,如果再次调用 get(),其行为将如同从未调用过 set() 一样(可能返回初始值或 null)。

关键点:

  • 一个 ThreadLocal 实例对应线程内部存储结构中的一个条目 (Entry)。如果你需要为每个线程存储多个不同类型或不同用途的数据,你需要创建多个 ThreadLocal 实例。
  • remove() 方法至关重要,尤其是在使用线程池时。线程池中的线程会被复用,如果不显式 remove(),上一个任务设置的 ThreadLocal 值可能会被下一个使用该线程的任务误读,并且导致内存泄漏。

# 2.3 ThreadLocal 工作原理解析

ThreadLocal 的魔法并不在 ThreadLocal 类本身,而是在 java.lang.Thread 类以及 ThreadLocal 的一个静态内部类 ThreadLocalMap 中。

核心机制: 每个 Thread 对象内部都有一个成员变量 threadLocals (以及另一个用于继承的 inheritableThreadLocals),这个变量的类型是 ThreadLocal.ThreadLocalMap。

// Thread 类内部结构 (简化示意)
public class Thread implements Runnable {
    // ... 其他成员变量 ...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null; // 存储普通 ThreadLocal 的 Map

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 存储可继承 ThreadLocal 的 Map

    // ... 构造函数、方法等 ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

ThreadLocalMap:

  • 它是一个定制化的哈希映射 (Map),专门用于存储线程本地变量。
  • 它的 Key 是 ThreadLocal 对象本身 (更准确地说,是 ThreadLocal 对象的弱引用 WeakReference<ThreadLocal<?>>)。
  • 它的 Value 就是通过 ThreadLocal.set(value) 设置的那个 value 对象。
  • ThreadLocalMap 的实例是存储在 Thread 对象内部的,而不是 ThreadLocal 对象内部。

# 2.3.1 set(T value) 方法原理

public void set(T value) {
    // 1. 获取当前正在执行的线程
    Thread t = Thread.currentThread();
    // 2. 获取当前线程内部的 threadLocals 这个 Map
    ThreadLocalMap map = getMap(t); // getMap(t) 实际上就是返回 t.threadLocals

    // 3. 判断 Map 是否已经存在
    if (map != null) {
        // 3.1 Map 已存在,直接将 <this ThreadLocal 对象, value> 存入 Map
        //     这里的 'this' 就是调用 set 方法的那个 ThreadLocal 实例
        map.set(this, value);
    } else {
        // 3.2 Map 不存在 (第一次为该线程设置 ThreadLocal 值)
        //     创建一个新的 ThreadLocalMap,并将 <this ThreadLocal 对象, value> 作为第一个条目放入
        createMap(t, value); // createMap(t, value) 内部会执行 t.threadLocals = new ThreadLocalMap(this, value);
    }
}

// ThreadLocalMap.set(key, value) 内部大致逻辑:
// - 计算 key (ThreadLocal 对象) 的哈希码
// - 根据哈希码找到或计算在内部 Entry[] 数组中的索引
// - 如果该位置为空,则创建新 Entry(key, value) 放入
// - 如果该位置已有 Entry:
//   - 若 Key 相同,则覆盖旧 Value
//   - 若 Key 不同 (哈希冲突),则使用线性探测法查找下一个可用位置
// - 在探测过程中,会清理 Key 为 null (弱引用已被回收) 的 "脏" Entry (expungeStaleEntry)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2.3.2 get() 方法原理

public T get() {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取当前线程的 threadLocals Map
    ThreadLocalMap map = getMap(t);

    // 3. 判断 Map 是否存在
    if (map != null) {
        // 3.1 Map 存在,尝试根据 'this' (当前 ThreadLocal 对象) 作为 Key 获取对应的 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 3.1.1 Entry 存在,直接返回 Entry 中存储的 Value
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }

    // 4. Map 不存在或 Map 中没有找到对应的 Entry
    //    调用 setInitialValue() 来获取/设置初始值
    return setInitialValue();
}

private T setInitialValue() {
    // 4.1 调用 initialValue() 方法获取初始值 (默认返回 null,可通过 withInitial 自定义)
    T value = initialValue(); // protected T initialValue() { return null; }
    // 4.2 获取当前线程和 Map (如果 Map 不存在则创建)
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 4.3 将初始值存入 Map
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    // 4.4 返回初始值
    return value;
}

// ThreadLocalMap.getEntry(key) 内部大致逻辑:
// - 计算 key 的哈希码,找到初始索引
// - 从该索引开始线性探测 Entry[] 数组:
//   - 如果找到 Entry 且其 Key (弱引用指向的对象) 与传入的 key 相同,返回该 Entry
//   - 如果找到 Entry 但其 Key 为 null (已被回收),调用 expungeStaleEntry 清理,然后继续探测
//   - 如果探测到 null (数组空位),说明 Key 不存在,返回 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 2.3.3 remove() 方法原理

public void remove() {
    // 1. 获取当前线程的 threadLocals Map
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 2. 如果 Map 存在
    if (m != null) {
        // 2.1 调用 Map 的 remove 方法,传入 'this' (当前 ThreadLocal 对象) 作为 Key
        m.remove(this);
    }
}

// ThreadLocalMap.remove(key) 内部大致逻辑:
// - 计算 key 的哈希码,找到初始索引
// - 从该索引开始线性探测:
//   - 如果找到 Entry 且其 Key 与传入的 key 相同:
//     - 将该 Entry 的 Key (弱引用) clear 掉
//     - 调用 expungeStaleEntry 清理该位置及其后续可能存在的脏条目
//     - 返回
//   - 如果探测到 null,说明 Key 不存在,直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.4 ThreadLocalMap 内部结构简述

  • Entry 数组: ThreadLocalMap 内部维护一个 Entry[] table 数组,用于存储键值对。数组大小总是 2 的幂。
  • Entry 类: static class Entry extends WeakReference<ThreadLocal<?>>。
    • 继承自 WeakReference,其引用的对象是 ThreadLocal 实例 (Key)。这意味着,即使 ThreadLocal 对象在外部没有强引用了,GC 也可以回收它。
    • 包含一个 Object value 成员,存储线程本地变量的值。
  • 哈希与冲突解决: 使用 threadLocalHashCode (与 ThreadLocal 对象关联的一个固定哈希值) 计算索引 i = key.threadLocalHashCode & (table.length - 1)。使用线性探测法 (Linear Probing) 解决哈希冲突。
  • 扩容: 当 Map 中元素数量超过阈值 (数组长度的 2/3) 时,会进行扩容 (rehash),通常是将数组大小翻倍。
  • 过期条目清理 (expungeStaleEntry): 在 set, get, remove 操作中,当遇到 Key 为 null 的 Entry (即 ThreadLocal 对象已被 GC 回收) 时,会触发清理逻辑,将该 Entry 的 value 设为 null,并可能重新整理 (rehash) 后续的冲突条目,以帮助释放 value 对象占用的内存。

# 2.5 设计演进:为什么是 Thread 持有 Map?

早期的 ThreadLocal 设计可能是每个 ThreadLocal 对象持有一个 Map<Thread, T>。但现在的设计 (Thread 持有 Map<ThreadLocal, T>) 更优:

  • 生命周期绑定: 当 Thread 结束时,其拥有的 ThreadLocalMap 自然也结束生命周期,有助于内存回收。
  • 减少 Map 数量: 通常 ThreadLocal 实例的数量远少于活跃线程的数量,每个线程持有一个 Map 比每个 ThreadLocal 持有一个 Map 更节省空间。
  • 分散存储: 数据分散存储在各个线程内部,访问时通常只需要访问当前线程的 Map,减少了潜在的锁竞争(虽然 ThreadLocalMap 本身的操作不是线程安全的,但它只被持有它的那个线程访问)。

# 3. ThreadLocal 内存泄漏问题深度剖析

虽然 ThreadLocal 提供了方便的线程数据隔离,但如果使用不当,尤其是在线程池场景下,可能会引发内存泄漏。

# 3.1 内存泄漏的原因

内存泄漏的核心在于 ThreadLocalMap 中 Entry 的设计以及 ThreadLocalMap 的生命周期:

  1. ThreadLocalMap 的生命周期与 Thread 绑定: 只要线程存活,ThreadLocalMap 就存活。
  2. Entry 的 Key 是弱引用: Entry(ThreadLocal<?> k, Object v) 中,k 是对 ThreadLocal 对象的弱引用 (WeakReference)。这意味着,当外部不再有强引用指向 ThreadLocal 对象时,GC 在下一次执行时会回收 ThreadLocal 对象本身,并将 Entry 中的弱引用 k 置为 null。
  3. Entry 的 Value 是强引用: Entry 中的 value 成员是对实际存储的线程本地变量值的强引用。

泄漏路径:

  • 当一个 ThreadLocal 对象不再被任何强引用指向时,GC 会回收它。
  • 此时,ThreadLocalMap 中对应的 Entry 的 Key (弱引用) 变为 null。
  • 但是,如果持有该 ThreadLocalMap 的线程仍然存活(比如线程池中的核心线程),并且代码中没有显式调用该 ThreadLocal 对象的 remove() 方法,那么这个 Key 为 null 的 Entry 仍然存在于 ThreadLocalMap 的 table 数组中。
  • 这个 Entry 对象本身不会被回收(因为它被 table 数组引用),更重要的是,它所持有的 value 对象(通过强引用)也不会被回收!
  • 如果这个 value 对象很大,或者不断有新的不再使用的 ThreadLocal 变量积累在线程的 Map 中,就会导致内存泄漏,最终可能引发 OutOfMemoryError。

图示:

弱引用内存泄漏示意图 (上图展示了 ThreadLocal 对象被回收后,Value 仍被 Entry 强引用的情况)

# 3.2 为什么设计成弱引用 Key?

既然弱引用 Key 不能完全避免内存泄漏,为什么还要这样设计?

弱引用提供了一种补救机制。ThreadLocalMap 在执行 set(), get(), remove() 操作时,会检查遇到的 Entry 的 Key 是否为 null。如果为 null,说明对应的 ThreadLocal 对象已被回收,此时 Map 会执行 expungeStaleEntry 操作,尝试将这个 Entry 的 value 设置为 null,并清理掉这个 "脏" Entry,从而使得 value 对象可以被 GC 回收。

但是,这种清理是被动的,它只在调用 set/get/remove 时顺带发生,并且不保证清理所有过期的 Entry。如果一个线程设置了 ThreadLocal 值后,长时间不再对该 ThreadLocal 或该 Map 进行任何操作,那么即使 ThreadLocal 对象被回收了,对应的 value 仍可能一直泄漏。

对比强引用 Key:

如果 Key 是强引用,那么只要线程存活,即使 ThreadLocal 对象在外部没有引用了,ThreadLocalMap 中的 Entry 对 ThreadLocal 对象的强引用也会阻止 ThreadLocal 对象被回收,进而 value 也无法回收,内存泄漏会更严重。

强引用内存泄漏示意图

# 3.3 防止内存泄漏的最佳实践:remove()

最根本、最可靠的防止 ThreadLocal 内存泄漏的方法是:在使用完 ThreadLocal 变量后,务必显式调用其 remove() 方法。

remove() 方法会直接将当前线程 ThreadLocalMap 中对应于该 ThreadLocal 实例的 Entry 清理掉(包括将 Key 和 Value 都置为 null),从而彻底断开 value 对象的强引用链,使其可以被 GC 正常回收。

尤其是在使用线程池的场景下,remove() 几乎是强制性的。 因为线程池会复用线程,如果不清理,线程执行完一个任务后,其 ThreadLocalMap 中存储的数据会遗留到下一个任务,不仅可能导致数据错乱,还会造成内存泄漏。

推荐使用 try-finally 结构确保 remove() 被执行:

// 获取或初始化 ThreadLocal 实例
private static final ThreadLocal<SomeResource> resourceThreadLocal = ThreadLocal.withInitial(SomeResource::new);

public void processRequest() {
    // 从 ThreadLocal 获取资源 (如果不存在则初始化)
    SomeResource resource = resourceThreadLocal.get();
    try {
        // --- 核心业务逻辑 ---
        // 使用 resource 对象...
        resource.doSomething();

    } finally {
        // --- 清理工作 ---
        // 无论业务逻辑是否成功或抛出异常,都确保移除 ThreadLocal 值
        resourceThreadLocal.remove();
        System.out.println("ThreadLocal resource removed for thread: " + Thread.currentThread().getName());
    }
}

// 模拟资源类
static class SomeResource {
    public SomeResource() { System.out.println("Creating SomeResource..."); }
    public void doSomething() { System.out.println("Doing something with resource..."); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4. ThreadLocal 的典型应用场景

ThreadLocal 的核心价值在于提供线程隔离的数据存储,非常适合以下场景:

# 4.1 场景一:管理每个请求的上下文信息(隐式参数传递)

在 Web 应用或分布式服务中,一个请求的处理可能跨越多个方法调用或多个服务层次(Controller -> Service -> DAO)。如果每个方法都需要获取当前请求的用户信息、追踪 ID (Trace ID)、租户 ID 等上下文信息,通过方法参数层层传递会非常繁琐且增加代码耦合。

ThreadLocal 可以完美解决这个问题:在请求开始时(如 Filter 或 Interceptor 中),将上下文信息存入 ThreadLocal;在调用链的任何地方,都可以方便地从中获取;在请求结束时(如 Filter 或 Interceptor 的 afterCompletion),务必清理 ThreadLocal。

/**
 * 用户上下文持有器
 */
public class UserContextHolder {
    // 使用 ThreadLocal 存储当前线程的用户信息
    private static final ThreadLocal<User> userContext = new ThreadLocal<>();

    // 设置当前线程的用户
    public static void setUser(User user) {
        if (user != null) {
            userContext.set(user);
        } else {
            userContext.remove(); // 如果传入 null,则认为是清除操作
        }
    }

    // 获取当前线程的用户
    public static User getUser() {
        return userContext.get();
    }

    // 清除当前线程的用户信息 (推荐使用此方法显式清除)
    public static void clear() {
        userContext.remove();
    }
}

// --- 在 Web Filter 或 Interceptor 中使用 ---
/*
public class ContextInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从请求中解析用户信息 (如 JWT Token, Session 等)
        User user = parseUserFromRequest(request);
        // 2. 将用户信息存入 ThreadLocal
        UserContextHolder.setUser(user);
        return true; // 继续处理请求
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 3. 请求处理完毕后,无论成功失败,务必清理 ThreadLocal
        UserContextHolder.clear();
    }
    // ...
}
*/

// --- 在 Service 层或任何需要的地方获取 ---
/*
public class OrderService {
    public void createOrder(Order order) {
        // 直接从 ThreadLocal 获取当前用户信息,无需方法参数传递
        User currentUser = UserContextHolder.getUser();
        if (currentUser != null) {
            order.setCreatorId(currentUser.getId());
            // ... 其他业务逻辑 ...
            System.out.println("User " + currentUser.getName() + " is creating order...");
        } else {
            // 处理未认证用户的情况
        }
    }
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

实际案例: Spring 框架的事务管理 (@Transactional) 就是一个经典例子。Spring 使用 ThreadLocal 来存储每个事务线程对应的数据库连接 (Connection),确保同一个事务内所有数据库操作都使用同一个连接,并在事务结束时关闭连接并清理 ThreadLocal。

# 4.2 场景二:存储用户身份认证信息

类似于上下文传递,可以在用户登录认证成功后,将用户的身份凭证(如用户 ID、角色列表等解析后的信息,注意不要存储敏感的原始密码或 Token)存入 ThreadLocal,方便后续业务逻辑进行权限判断或记录操作日志。

import java.util.Map;
import java.util.HashMap;

/**
 * 存储当前线程认证信息的持有器
 */
public class AuthInfoHolder {
    // 使用 ThreadLocal 存储 Map,可以存放多项认证信息
    private static final ThreadLocal<Map<String, Object>> authInfoContext = ThreadLocal.withInitial(HashMap::new);

    /**
     * 存储认证信息键值对
     * @param key   信息键,如 "userId", "roles", "tenantId"
     * @param value 信息值
     */
    public static void set(String key, Object value) {
        authInfoContext.get().put(key, value);
    }

    /**
     * 批量设置认证信息
     * @param infoMap 包含认证信息的 Map
     */
    public static void setAll(Map<String, Object> infoMap) {
        if (infoMap != null) {
            // 创建一个新的 Map 副本存入,避免外部修改影响 ThreadLocal 中的值
            authInfoContext.set(new HashMap<>(infoMap));
        } else {
             authInfoContext.remove();
        }
    }


    /**
     * 根据键获取认证信息
     * @param key 信息键
     * @return 对应的值,如果不存在则返回 null
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(String key) {
        return (T) authInfoContext.get().get(key);
    }

    /**
     * 获取当前用户的 ID
     * @return 用户 ID,可能为 null
     */
    public static Long getUserId() {
        return get("userId");
    }

    /**
     * 获取所有认证信息
     * @return 包含所有认证信息的 Map 副本
     */
    public static Map<String, Object> getAll() {
        // 返回副本,防止外部修改
        return new HashMap<>(authInfoContext.get());
    }

    /**
     * 清除当前线程的所有认证信息
     */
    public static void clear() {
        authInfoContext.remove();
    }
}

// --- 在认证 Filter/Interceptor 中使用 ---
/*
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    // 1. 验证 Token 或 Session ...
    if (authenticationSuccessful) {
        // 2. 解析认证信息
        Map<String, Object> authInfo = parseAuthInfo(credentials);
        // 3. 存入 ThreadLocal
        AuthInfoHolder.setAll(authInfo);
        return true;
    }
    // ... 认证失败处理 ...
    return false;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    // 4. 清理 ThreadLocal
    AuthInfoHolder.clear();
}
*/

// --- 在需要权限校验或记录日志的地方使用 ---
/*
public void sensitiveOperation() {
    Long currentUserId = AuthInfoHolder.getUserId();
    List<String> roles = AuthInfoHolder.get("roles");
    if (roles != null && roles.contains("ADMIN")) {
        // ... 执行管理员操作 ...
        log.info("Admin operation performed by user: {}", currentUserId);
    } else {
        throw new AccessDeniedException("Permission denied for user: " + currentUserId);
    }
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

# 4.3 场景三:解决非线程安全的类在并发环境下的使用问题

有些类(如 java.text.SimpleDateFormat)本身不是线程安全的,如果在多线程环境中共享同一个实例,可能会导致解析或格式化结果出错。

ThreadLocal 可以为此类场景提供解决方案:为每个线程创建一个独立的实例,存放在 ThreadLocal 中。这样每个线程都使用自己的实例,避免了线程安全问题,同时也避免了每次使用时都创建新对象的开销。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 线程安全的日期格式化工具类
 */
public class SafeDateFormatter {

    // 使用 ThreadLocal 为每个线程提供独立的 SimpleDateFormat 实例
    // 通过 withInitial 设置初始值,当线程第一次调用 get() 时会自动创建
    private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =
            ThreadLocal.withInitial(() -> {
                System.out.println("Creating SimpleDateFormat for thread: " + Thread.currentThread().getName());
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            });

    /**
     * 将 Date 对象格式化为字符串
     * @param date 要格式化的日期
     * @return 格式化后的字符串
     */
    public static String format(Date date) {
        // 从 ThreadLocal 获取当前线程对应的 SimpleDateFormat 实例
        return sdfThreadLocal.get().format(date);
    }

    /**
     * 将字符串解析为 Date 对象
     * @param dateString 要解析的日期字符串
     * @return 解析后的 Date 对象
     * @throws ParseException 如果解析失败
     */
    public static Date parse(String dateString) throws ParseException {
        // 从 ThreadLocal 获取当前线程对应的 SimpleDateFormat 实例
        return sdfThreadLocal.get().parse(dateString);
    }

    // 无需 remove() 方法,因为 SimpleDateFormat 对象通常希望与线程生命周期一致
    // 除非有特殊需要(如切换格式),一般不移除

    public static void main(String[] args) {
        // 模拟多线程并发使用
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Date now = new Date();
                String formatted = SafeDateFormatter.format(now);
                System.out.println(Thread.currentThread().getName() + " formatted: " + formatted);
                try {
                    Date parsed = SafeDateFormatter.parse(formatted);
                    System.out.println(Thread.currentThread().getName() + " parsed: " + parsed);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i).start();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

注意: 对于 SimpleDateFormat 这类场景,Java 8 引入的 java.time.format.DateTimeFormatter 本身就是线程安全的,是更好的选择。此示例主要用于演示 ThreadLocal 如何解决非线程安全对象的并发使用问题。

# 5. InheritableThreadLocal:让子线程继承父线程的值

标准的 ThreadLocal 变量的值在父子线程之间是不能传递的。即,如果父线程设置了一个 ThreadLocal 值,然后创建了一个子线程,子线程通过 get() 方法获取到的将是 null (或初始值),而不是父线程设置的值。

为了解决这个问题,Java 提供了 InheritableThreadLocal。

# 5.1 InheritableThreadLocal 介绍

InheritableThreadLocal 是 ThreadLocal 的一个子类。当父线程创建一个子线程时,InheritableThreadLocal 会自动将父线程中存储的值复制一份给子线程。

关键特性:

  • 子线程创建时,会继承父线程在 InheritableThreadLocal 变量上的值。
  • 这种继承是值复制 (浅拷贝),不是共享引用。子线程后续对该值的修改不会影响父线程。
  • 父线程在子线程创建之后对 InheritableThreadLocal 值的修改,不会影响已经创建的子线程。

# 5.2 ThreadLocal vs InheritableThreadLocal 对比

# ThreadLocal 在父子线程中的表现

/**
 * 演示 ThreadLocal 在父子线程中的行为
 */
public class ThreadLocalParentChildDemo {
    // 创建一个普通的 ThreadLocal
    private static ThreadLocal<String> normalThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        // 主线程 (父线程) 设置值
        normalThreadLocal.set("Parent Value - Normal");
        System.out.println("Parent thread gets: " + normalThreadLocal.get()); // 输出: Parent Value - Normal

        // 创建并启动子线程
        Thread childThread = new Thread(() -> {
            // 子线程尝试获取值
            System.out.println("Child thread gets: " + normalThreadLocal.get()); // 输出: null
            // 子线程设置自己的值
            normalThreadLocal.set("Child Value - Normal");
            System.out.println("Child thread sets and gets: " + normalThreadLocal.get()); // 输出: Child Value - Normal
        }, "ChildThread-Normal");
        childThread.start();
        childThread.join(); // 等待子线程结束

        // 父线程再次获取值,不受子线程影响
        System.out.println("Parent thread gets again: " + normalThreadLocal.get()); // 输出: Parent Value - Normal

        normalThreadLocal.remove(); // 清理
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# InheritableThreadLocal 在父子线程中的表现

/**
 * 演示 InheritableThreadLocal 在父子线程中的行为
 */
public class InheritableThreadLocalParentChildDemo {
    // 创建一个 InheritableThreadLocal
    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    // 如果需要自定义继承时的值处理逻辑,可以重写 childValue 方法
    private static InheritableThreadLocal<StringBuilder> customInheritable = new InheritableThreadLocal<StringBuilder>() {
        @Override
        protected StringBuilder childValue(StringBuilder parentValue) {
            // 当子线程创建时,此方法被调用以决定子线程的值
            // 这里返回父值的副本,并追加 "(copied)"
            return parentValue == null ? null : new StringBuilder(parentValue).append("(copied)");
        }
    };

    public static void main(String[] args) throws InterruptedException {
        // --- 基本继承 ---
        System.out.println("--- Basic Inheritance ---");
        // 父线程设置值
        inheritableThreadLocal.set("Parent Value - Inheritable");
        System.out.println("Parent thread gets: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable

        // 创建并启动子线程
        Thread childThread1 = new Thread(() -> {
            // 子线程获取值 (继承自父线程)
            System.out.println("Child thread 1 gets: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable
            // 子线程修改自己的值
            inheritableThreadLocal.set("Child Value - Inheritable");
            System.out.println("Child thread 1 sets and gets: " + inheritableThreadLocal.get()); // 输出: Child Value - Inheritable
        }, "ChildThread-Inheritable-1");
        childThread1.start();
        childThread1.join();

        // 父线程再次获取值,不受子线程影响
        System.out.println("Parent thread gets again: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable
        inheritableThreadLocal.remove();

        // --- 演示父线程后续修改不影响已创建的子线程 ---
        System.out.println("\n--- Parent Modification After Child Creation ---");
        inheritableThreadLocal.set("Parent Value - Before Child 2");
        Thread childThread2 = new Thread(() -> {
            System.out.println("Child thread 2 gets (initially): " + inheritableThreadLocal.get()); // 输出: Parent Value - Before Child 2
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            System.out.println("Child thread 2 gets (later): " + inheritableThreadLocal.get()); // 仍然是: Parent Value - Before Child 2
        }, "ChildThread-Inheritable-2");
        childThread2.start();
        Thread.sleep(50); // 确保子线程已启动并获取了初始值
        inheritableThreadLocal.set("Parent Value - After Child 2 Started"); // 父线程修改值
        System.out.println("Parent thread gets after modification: " + inheritableThreadLocal.get()); // 输出: Parent Value - After Child 2 Started
        childThread2.join();
        inheritableThreadLocal.remove();

        // --- 演示自定义 childValue ---
        System.out.println("\n--- Custom childValue Inheritance ---");
        StringBuilder parentSb = new StringBuilder("ParentSB");
        customInheritable.set(parentSb);
        System.out.println("Parent thread gets StringBuilder: " + customInheritable.get());
        Thread childThread3 = new Thread(() -> {
            System.out.println("Child thread 3 gets StringBuilder: " + customInheritable.get()); // 输出: ParentSB(copied)
            // 修改子线程的值
            customInheritable.get().append("-ChildModified");
            System.out.println("Child thread 3 modified value: " + customInheritable.get());
        }, "ChildThread-Custom");
        childThread3.start();
        childThread3.join();
        // 父线程的值未受影响
        System.out.println("Parent thread gets StringBuilder again: " + customInheritable.get()); // 输出: ParentSB
        customInheritable.remove();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 5.3 InheritableThreadLocal 工作原理

InheritableThreadLocal 利用了 Thread 类中的另一个 ThreadLocalMap 成员变量:inheritableThreadLocals。

// Thread 类内部
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
1
2

当使用 InheritableThreadLocal 的 set/get/remove 方法时,实际操作的是当前线程的 inheritableThreadLocals 这个 Map。

关键在于线程创建:

当一个新的 Thread 对象被创建时,在其 init 方法(由构造函数调用)中,会检查父线程 (parent) 的 inheritableThreadLocals 是否为 null 并且是否需要继承 (inheritThreadLocals 标志位,默认为 true)。如果需要继承,则会调用 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals) 来创建一个子线程的 inheritableThreadLocals Map。

// Thread 类 init 方法的部分逻辑 (简化)
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    // ... 其他初始化 ...
    Thread parent = currentThread(); // 获取父线程
    // ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
        // 如果需要继承且父线程有可继承的 Map
        // 则为子线程创建一个新的 Map,其内容基于父线程的 Map
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
    // ...
}

// ThreadLocal.createInheritedMap 静态方法 (简化逻辑)
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    // 创建一个新的 ThreadLocalMap,其初始内容是父 Map 中每个 Entry 的副本
    // 对于每个 Entry,会调用 InheritableThreadLocal 的 childValue 方法来决定子线程的值
    return new ThreadLocalMap(parentMap);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

createInheritedMap 的过程大致是遍历父 Map 的所有 Entry,对于每个 Entry,获取其 Key (InheritableThreadLocal 实例) 和 Value。然后调用 Key (即 InheritableThreadLocal 对象) 的 childValue(parentValue) 方法,用其返回值作为子线程 Map 中该 Key 对应的新 Value。默认的 childValue 方法直接返回 parentValue (浅拷贝)。

# 5.4 InheritableThreadLocal 在线程池中的局限性

InheritableThreadLocal 最大的问题在于它与线程池一起使用时。线程池的核心机制是线程复用。

  • 当一个任务提交给线程池时,它会被一个池中的工作线程执行。
  • 这个工作线程可能之前已经执行过其他任务。
  • InheritableThreadLocal 的值传递只发生在线程创建时 (new Thread())。
  • 线程池中的线程一旦创建,就不会再重新执行 init 方法来从“父线程”(即提交任务的那个线程)那里继承 ThreadLocal 值了。

后果:

  1. 无法获取最新值: 如果提交任务的线程在提交第一个任务后,修改了 InheritableThreadLocal 的值,然后提交第二个任务到同一个工作线程,第二个任务获取到的仍然是工作线程第一次被创建时从当时的父线程继承来的旧值,而不是提交任务线程的最新值。
  2. 数据污染: 工作线程执行完一个任务后,其 inheritableThreadLocals Map 中的值会保留下来。如果这个线程接着执行另一个任务,而这个新任务期望的上下文与旧值不同,就会发生数据污染。虽然 remove() 可以解决部分问题,但无法解决无法获取提交者最新值的问题。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 演示 InheritableThreadLocal 在线程池中的限制
 */
public class InheritableThreadPoolLimitationDemo {

    // 定义 InheritableThreadLocal
    private static InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
    // 创建一个单线程的线程池,确保线程被复用
    private static ExecutorService threadPool = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Main thread starts.");

        // 主线程设置初始值
        context.set("Value from Main - Task 1");
        System.out.println("Main thread sets context: " + context.get());

        // 提交第一个任务
        threadPool.submit(() -> {
            // 工作线程获取的值是提交任务时主线程的值 (通过线程创建时继承,这里模拟)
            System.out.println("Worker thread (Task 1) gets context: " + context.get()); // 输出: Value from Main - Task 1
            // 工作线程修改自己的值
            context.set("Value set by Worker in Task 1");
            System.out.println("Worker thread (Task 1) sets context: " + context.get());
            // 此处没有调用 remove(),模拟值残留
        });

        // 等待第一个任务执行完毕
        TimeUnit.MILLISECONDS.sleep(100); // 简单等待,实际应使用 Future 等待

        // 主线程修改值
        context.set("Value from Main - Task 2");
        System.out.println("\nMain thread updates context: " + context.get());

        // 提交第二个任务 (复用同一个工作线程)
        threadPool.submit(() -> {
            // 工作线程获取的值是上一个任务残留的值,而不是主线程的最新值!
            System.out.println("Worker thread (Task 2) gets context: " + context.get()); // !! 输出: Value set by Worker in Task 1 !!
                                                                                    // !! 而不是 "Value from Main - Task 2" !!
        });

        // 关闭线程池
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("\nMain thread finished.");
        context.remove(); // 清理主线程的
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 6. TransmittableThreadLocal (TTL):线程池上下文传递的终极方案

为了解决 InheritableThreadLocal 在线程池等异步执行场景下的上下文传递问题,阿里巴巴开源了 TransmittableThreadLocal (TTL)。

# 6.1 TTL 介绍

TTL 是一个旨在增强 InheritableThreadLocal 的库,它确保在使用线程池、@Async 注解、CompletableFuture 等异步执行模型时,能够可靠地将父线程(提交任务的线程)的 ThreadLocal 上下文传递给子线程(执行任务的线程)。

核心目标: 让开发者感觉就像在使用普通的 InheritableThreadLocal 一样简单,但在异步场景下也能正确工作。

# 6.2 TTL 工作原理

TTL 的实现比 InheritableThreadLocal 更复杂,它通过装饰 (Decorate) Java 标准库中的相关类(如 Runnable, Callable, ExecutorService)来实现上下文的捕获和传递:

  1. 上下文捕获 (Capture): 在提交任务时(例如调用 ExecutorService.submit() 之前),TTL 会捕获当前线程中所有 TransmittableThreadLocal 实例及其对应的值,形成一个“快照 (Snapshot)”。
  2. 上下文传递 (Transmit): 这个“快照”会与被提交的任务(如 Runnable 或 Callable)关联起来。TTL 提供了包装类(如 TtlRunnable, TtlCallable)来持有这个快照。
  3. 上下文回放 (Replay / Restore): 在任务执行时(即在线程池的工作线程中执行 run() 或 call() 方法之前),TTL 的包装类会执行 replay 操作:将快照中的 ThreadLocal 值设置到当前工作线程的 ThreadLocalMap 中。
  4. 上下文恢复 (Backup / Restore): 在任务执行完毕后(run() 或 call() 方法执行结束,无论成功还是异常),TTL 会执行 restore 操作:恢复工作线程在执行任务之前的原始 ThreadLocal 状态(清除掉 replay 进去的值,恢复原来的值)。

时序流程示意:

TTL时序图

这种机制确保了:

  • 执行任务的线程能获取到提交任务时的上下文信息。
  • 任务执行期间对 ThreadLocal 的修改不会污染线程池中的线程,因为执行完后会恢复原状。

# 6.3 TTL 基本使用

首先,需要添加 TTL 的 Maven 依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <!-- 使用最新稳定版本 -->
    <version>2.14.5</version>
</dependency>
1
2
3
4
5
6

然后,将需要传递的 ThreadLocal 变量定义为 TransmittableThreadLocal 类型,并在提交异步任务时使用 TTL 提供的包装器。

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable; // TTL 提供的 Runnable 包装类
import com.alibaba.ttl.TtlCallable; // TTL 提供的 Callable 包装类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * 演示 TransmittableThreadLocal 在线程池中的使用
 */
public class TransmittableThreadPoolDemo {

    // 1. 定义 TransmittableThreadLocal 变量
    private static TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
    // 创建线程池
    private static ExecutorService threadPool = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws Exception {
        System.out.println("Main thread starts.");

        // 主线程设置初始值
        context.set("Value from Main - Task 1 (TTL)");
        System.out.println("Main thread sets context: " + context.get());

        // 2. 使用 TtlRunnable.get() 包装 Runnable 任务
        Runnable task1 = () -> {
            // 工作线程能正确获取到提交任务时主线程的值
            System.out.println("Worker thread (Task 1) gets context: " + context.get()); // 输出: Value from Main - Task 1 (TTL)
            context.set("Value set by Worker in Task 1 (TTL)"); // 工作线程修改自己的上下文
            System.out.println("Worker thread (Task 1) sets context: " + context.get());
        };
        Runnable ttlTask1 = TtlRunnable.get(task1); // 获取包装后的 Runnable

        // 提交包装后的任务
        threadPool.submit(ttlTask1);
        TimeUnit.MILLISECONDS.sleep(100); // 等待任务 1 完成

        // 主线程修改值
        context.set("Value from Main - Task 2 (TTL)");
        System.out.println("\nMain thread updates context: " + context.get());

        // 3. 使用 TtlCallable.get() 包装 Callable 任务
        Callable<String> task2 = () -> {
            // 工作线程能正确获取到提交任务时主线程的最新值
            String receivedValue = context.get();
            System.out.println("Worker thread (Task 2) gets context: " + receivedValue); // 输出: Value from Main - Task 2 (TTL)
            return "Task 2 result, received: " + receivedValue;
        };
        Callable<String> ttlTask2 = TtlCallable.get(task2); // 获取包装后的 Callable

        // 提交包装后的任务并获取结果
        Future<String> future = threadPool.submit(ttlTask2);
        System.out.println("Task 2 Future result: " + future.get());

        // 4. 验证线程池线程状态是否被恢复 (提交一个普通任务)
        threadPool.submit(() -> {
             System.out.println("\nWorker thread (Plain Task) gets context after TTL tasks: " + context.get()); // 输出: null (或初始值),证明 TTL 成功恢复了线程状态
        });


        // 关闭线程池
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("\nMain thread finished.");
        context.remove(); // 清理主线程的
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

输出结果对比: 可以看到,使用 TTL 后,任务 2 能够正确获取到主线程更新后的值 Value from Main - Task 2 (TTL),并且普通任务运行时线程的上下文是干净的 (null),证明了 TTL 的有效性。

# 6.4 TTL 进阶:装饰线程池与自定义传递

1. 装饰 ExecutorService (推荐)

手动包装每个 Runnable/Callable 比较繁琐。TTL 提供了工具类 TtlExecutors 来直接装饰(包装)整个 ExecutorService 实例。之后通过这个装饰后的 ExecutorService 提交任务,就无需再手动包装 Runnable/Callable 了,TTL 会自动处理。

import com.alibaba.ttl.threadpool.TtlExecutors; // 用于装饰线程池的工具类
// ... 其他 import ...

public class TtlExecutorDecoratorDemo {
    private static TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
    // 创建原始线程池
    private static ExecutorService originalExecutor = Executors.newFixedThreadPool(1);
    // 使用 TtlExecutors 装饰原始线程池
    private static ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor);

    public static void main(String[] args) throws Exception {
        context.set("Value via Decorated Executor");
        System.out.println("Main thread context: " + context.get());

        // 直接使用装饰后的 ttlExecutor 提交【未经包装】的 Runnable
        ttlExecutor.submit(() -> {
            System.out.println("Worker thread (via decorated executor) gets context: " + context.get());
            // 输出: Value via Decorated Executor
        });

        // 关闭原始线程池和装饰后的线程池(通常只需关闭原始的)
        originalExecutor.shutdown();
        originalExecutor.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("\nMain thread finished.");
        context.remove();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

注意: TTL 还支持对 ForkJoinPool, TimerTask 等进行类似的装饰。

2. 自定义值的复制逻辑 (深拷贝)

InheritableThreadLocal 和 TransmittableThreadLocal 默认都是进行浅拷贝。如果 ThreadLocal 中存储的是可变对象(如 List, Map, 自定义对象),父子线程(或任务提交者与执行者)获取到的是指向同一个可变对象的引用(或其浅拷贝)。一方修改会影响另一方。

如果需要深拷贝(即子线程/任务执行者获取到一个完全独立的对象副本),可以重写 TransmittableThreadLocal 的 copyValue 方法 (类似于 InheritableThreadLocal 的 childValue)。

import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;

/**
 * 演示 TTL 自定义深拷贝逻辑
 */
public class TtlDeepCopyDemo {

    // 定义一个存储可变对象 (Map) 的 TTL,并重写 copyValue 实现深拷贝
    private static TransmittableThreadLocal<Map<String, String>> mapContext =
        new TransmittableThreadLocal<Map<String, String>>() {
            /**
             * 在捕获上下文时调用此方法,用于创建值的副本。
             * @param parentValue 父线程(或提交任务线程)的值
             * @return 要传递给子线程(或执行任务线程)的值副本
             */
            @Override
            protected Map<String, String> copyValue(Map<String, String> parentValue) {
                System.out.println("Copying map for transmission...");
                if (parentValue == null) {
                    return null;
                }
                // 创建一个新的 HashMap,实现深拷贝
                return new HashMap<>(parentValue);
            }
        };

    public static void main(String[] args) {
        // 主线程设置 Map
        Map<String, String> mainMap = new HashMap<>();
        mainMap.put("key1", "value1");
        mapContext.set(mainMap);
        System.out.println("Main thread initial map: " + mapContext.get());

        // 启动一个新线程 (TTL 对 Thread 也有效,如果父线程的 mapContext 非 null)
        Thread childThread = new Thread(() -> {
            Map<String, String> childMap = mapContext.get();
            System.out.println("Child thread received map: " + childMap);
            // 修改子线程的 Map
            childMap.put("key2", "value2_child");
            System.out.println("Child thread modified map: " + childMap);
        });
        childThread.start();
        try { childThread.join(); } catch (InterruptedException e) {}

        // 验证主线程的 Map 是否受影响
        System.out.println("Main thread map after child modification: " + mapContext.get());
        // 输出: {key1=value1} (未受影响,因为传递的是深拷贝)

        mapContext.remove();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 7. ThreadLocal 使用总结与建议

ThreadLocal 及其变种是处理线程数据隔离的强大工具,但理解其工作原理和潜在陷阱至关重要。

核心要点回顾:

  1. 作用: 提供线程本地变量,实现线程间数据隔离,避免同步开销。核心思想是“空间换时间”。
  2. 原理: 每个 Thread 对象持有 ThreadLocalMap,Key 是 ThreadLocal 对象的弱引用,Value 是实际存储的值。
  3. 内存泄漏: 主要风险在于 ThreadLocalMap 生命周期与线程绑定,且 Key 被回收后 Value 仍被强引用。必须通过显式调用 remove() 方法来避免。
  4. InheritableThreadLocal: 允许子线程在创建时继承父线程的值(浅拷贝),但在线程池复用场景下失效。
  5. TransmittableThreadLocal (TTL): 解决了 InheritableThreadLocal 在线程池等异步场景下的问题,通过上下文捕获、传递和恢复机制,确保上下文正确传递。推荐在异步环境中使用。

使用建议:

  1. 明确目的: 确认你的场景确实需要线程隔离的数据,而不是需要线程间共享和同步的数据。
  2. 强制 remove(): 养成在使用完 ThreadLocal (特别是 ThreadLocal 和 InheritableThreadLocal) 后调用 remove() 的习惯,使用 try-finally 保证执行。
  3. 线程池场景优先 TTL: 在涉及线程池、@Async、CompletableFuture 等异步执行模型时,优先使用 TransmittableThreadLocal 并考虑装饰 ExecutorService。
  4. 谨慎存储大对象: ThreadLocal 不适合存储生命周期长且占用内存大的对象,否则可能加剧内存消耗和泄漏风险。
  5. 注意可变对象: 如果存储的是可变对象,默认的浅拷贝(InheritableThreadLocal / TTL)可能导致意想不到的副作用。如果需要完全隔离,应实现深拷贝逻辑(重写 childValue / copyValue)。
  6. 命名清晰: 给 ThreadLocal 变量起一个能清晰反映其用途的名字。
  7. 封装使用: 可以将 ThreadLocal 的操作封装在工具类(如 UserContextHolder)中,统一管理 set, get, remove 逻辑,降低直接操作 ThreadLocal 的风险。

通过遵循这些原则和最佳实践,你可以安全、高效地利用 ThreadLocal 及其相关技术来简化并发编程,提升应用程序的性能和可维护性。

编辑此页 (opens new window)
上次更新: 2025/04/06, 10:15:45
Guava(开发工具包)
SLF4j(日志框架)

← Guava(开发工具包) SLF4j(日志框架)→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式