乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

ThreadLocal详解

飞翔科技故障复盘会,周一早晨。

朱璐(测试组长,盯着 OOM 堆栈):"生产环境又 Full GC 了!MAT 分析显示一堆 ThreadLocalMap$Entry 占用了 2GB 老年代,但是那些线程明明已经结束了啊!"

大翔(CTO,揉着太阳穴):"又是 ThreadLocal 内存泄漏的问题。上周刚修的,怎么又犯了?"

小崔(初级开发,愧疚):"是我...我在拦截器里用 ThreadLocal 存用户信息,用完忘记调 remove() 了。"

白歌(首席架构师):"ThreadLocal 是双刃剑——用好了优雅解耦(线程级别全局变量),用不好就是内存泄漏炸弹。特别是配合线程池的时候,线程复用导致 ThreadLocalMap 里的 Entry 永远不会被清理。"

Frank(后端专家,在白板上画内存模型):"The key insight is WeakReference. ThreadLocalMap's Entry extends WeakReference. The key (ThreadLocal object) is weak, but the value is strong. If the ThreadLocal object gets GC'd but the thread lives on (thread pool!), the value is leaked."

大翔:"好,今天复盘会就变成技术分享会。小崔,你来讲讲 ThreadLocal 从原理到泄漏再到修复——讲不清楚这周请全组喝奶茶。"


一、定义表

概念定义JDK 8 关键类/方法
ThreadLocal线程局部变量:每个线程拥有独立的变量副本,线程间互不干扰java.lang.ThreadLocal<T>
ThreadLocalMapThreadLocal 的内部静态类,实际存储线程局部变量的容器ThreadLocal.ThreadLocalMap
Entry (WeakReference)ThreadLocalMap 的节点:key 是 WeakReference<ThreadLocal>,value 是强引用ThreadLocalMap.Entry
内存泄漏key(ThreadLocal)被 GC 后,value 仍有强引用链(Thread → ThreadLocalMap → Entry → value),无法回收—
InheritableThreadLocal子线程可以继承父线程的 ThreadLocal 值java.lang.InheritableThreadLocal&lt;T&gt;
initialValue()首次 get() 时若值为 null,调用此方法初始化ThreadLocal.initialValue()
withInitial()JDK 8 新增的工厂方法,用 Lambda 提供初始值ThreadLocal.withInitial(Supplier)

二、ThreadLocal 核心原理

2.1 数据结构关系图

2.2 get() 方法执行流程


三、内存泄漏原理详解

3.1 引用链分析

Thread (强引用)
  → ThreadLocalMap threadLocals (强引用)
    → Entry[] table (强引用)
      → Entry (key=WeakReference&lt;ThreadLocal&gt;, value=强引用)
         ├─ key: ThreadLocal ← WeakReference (GC时断开)
         └─ value: Object ← 强引用 (GC时不回收!)

关键:当 ThreadLocal 对象没有外部强引用时,它会被 GC 回收。但 Entry 中的 value 通过 Thread → ThreadLocalMap → Entry → value 这条强引用链仍然可达,不会被回收,造成内存泄漏。

3.2 线程池场景加剧泄漏


四、InheritableThreadLocal —— 父子线程传递

// 父线程设置值
InheritableThreadLocal&lt;String&gt; context = new InheritableThreadLocal<>();
context.set("飞翔科技-大翔");

// 子线程可以继承父线程的值
new Thread(() -> {
    System.out.println(context.get());  // "飞翔科技-大翔"
}).start();

注意:InheritableThreadLocal 在子线程创建时(Thread.init())复制父线程的值,之后父子线程的值互相独立。线程池场景下,线程复用会导致"继承"行为不可控。


五、代码示例

示例一:飞翔科技请求上下文传递——ThreadLocal 封装用户信息

场景:飞翔科技的 Web 应用中,Controller → Service → DAO 每层都需要获取当前登录用户的信息。如果层层传参会造成代码污染,使用 ThreadLocal 实现优雅的上下文传递。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 飞翔科技 —— 请求上下文用户信息传递(ThreadLocal)
 */
public class RequestContextHolder {

    // 用户信息实体
    static class UserInfo {
        private final long userId;
        private final String userName;
        private final String role;
        private final String department;

        public UserInfo(long userId, String userName, String role, String department) {
            this.userId = userId;
            this.userName = userName;
            this.role = role;
            this.department = department;
        }

        @Override
        public String toString() {
            return String.format("UserInfo{id=%d, name='%s', role='%s', dept='%s'}",
                    userId, userName, role, department);
        }
    }

    // 请求上下文持有者 —— 线程级别"全局变量"
    static class UserContext {
        // JDK 8 推荐: 使用 withInitial 提供初始值
        private static final ThreadLocal&lt;UserInfo&gt; currentUser =
                ThreadLocal.withInitial(() -> null);

        public static void set(UserInfo user) {
            currentUser.set(user);
        }

        public static UserInfo get() {
            return currentUser.get();
        }

        public static void clear() {
            currentUser.remove();  // 关键!防止内存泄漏
        }

        public static long getUserId() {
            UserInfo user = get();
            return user != null ? user.userId : -1;
        }

        public static String getUserName() {
            UserInfo user = get();
            return user != null ? user.userName : "anonymous";
        }
    }

    // 模拟业务层:Service
    static class OrderService {
        public void createOrder(String product, int quantity) {
            UserInfo user = UserContext.get();
            System.out.printf("[OrderService] 用户 %s (部门:%s) 创建订单: %s x %d%n",
                    user.userName, user.department, product, quantity);

            // 调用 DAO 层
            new OrderDAO().insertOrder(product, quantity);
        }
    }

    // 模拟数据访问层:DAO
    static class OrderDAO {
        public void insertOrder(String product, int quantity) {
            UserInfo user = UserContext.get();
            System.out.printf("[OrderDAO] 操作员: %s (ID:%d), 写入数据库: %s x %d%n",
                    user.userName, user.userId, product, quantity);
            // 权限校验
            if ("intern".equals(user.role)) {
                System.out.println("[OrderDAO] ⚠ 实习生无权创建订单!");
                return;
            }
            System.out.println("[OrderDAO] 订单写入成功 ✓");
        }
    }

    // 模拟拦截器/过滤器
    static class AuthInterceptor {
        public static void handleRequest(UserInfo user, Runnable businessLogic) {
            try {
                // 1. 请求进入时设置上下文
                UserContext.set(user);
                System.out.printf("[AuthInterceptor] 请求进入 — 当前用户: %s%n", user.userName);

                // 2. 执行业务逻辑
                businessLogic.run();

            } finally {
                // 3. 请求结束时清理 —— 关键!
                System.out.printf("[AuthInterceptor] 请求结束 — 清理用户: %s%n",
                        UserContext.getUserName());
                UserContext.clear();
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("===== 飞翔科技订单系统(ThreadLocal 上下文传递) =====\n");

        // 使用线程池模拟并发请求
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 请求1: 大翔(CTO)创建订单
        executor.execute(() -> {
            AuthInterceptor.handleRequest(
                    new UserInfo(1, "大翔", "cto", "技术部"),
                    () -> new OrderService().createOrder("MacBook Pro", 10)
            );
        });

        // 请求2: 白歌(架构师)创建订单
        executor.execute(() -> {
            AuthInterceptor.handleRequest(
                    new UserInfo(2, "白歌", "architect", "架构组"),
                    () -> new OrderService().createOrder("iPad Pro", 5)
            );
        });

        // 请求3: 小崔(实习生)创建订单——权限不足
        executor.execute(() -> {
            AuthInterceptor.handleRequest(
                    new UserInfo(3, "小崔", "intern", "开发组"),
                    () -> new OrderService().createOrder("iPhone 15", 1)
            );
        });

        executor.shutdown();
        while (!executor.isTerminated()) {
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }

        System.out.println("\n===== 所有请求处理完毕 =====");
        // 验证:此时各线程的 ThreadLocal 已被清理
        System.out.println("主线程用户上下文: " + UserContext.get());  // null
    }
}

控制台输出:

===== 飞翔科技订单系统(ThreadLocal 上下文传递) =====

[AuthInterceptor] 请求进入 — 当前用户: 大翔
[AuthInterceptor] 请求进入 — 当前用户: 白歌
[AuthInterceptor] 请求进入 — 当前用户: 小崔
[OrderService] 用户 大翔 (部门:技术部) 创建订单: MacBook Pro x 10
[OrderDAO] 操作员: 大翔 (ID:1), 写入数据库: MacBook Pro x 10
[OrderDAO] 订单写入成功 ✓
[AuthInterceptor] 请求结束 — 清理用户: 大翔
[OrderService] 用户 小崔 (部门:开发组) 创建订单: iPhone 15 x 1
[OrderDAO] 操作员: 小崔 (ID:3), 写入数据库: iPhone 15 x 1
[OrderDAO] ⚠ 实习生无权创建订单!
[AuthInterceptor] 请求结束 — 清理用户: 小崔
[OrderService] 用户 白歌 (部门:架构组) 创建订单: iPad Pro x 5
[OrderDAO] 操作员: 白歌 (ID:2), 写入数据库: iPad Pro x 5
[OrderDAO] 订单写入成功 ✓
[AuthInterceptor] 请求结束 — 清理用户: 白歌

===== 所有请求处理完毕 =====
主线程用户上下文: null

示例二:数据库连接绑定——ThreadLocal 实现线程级连接隔离

场景:飞翔科技的 DAO 层需要保证同一个线程内的所有数据库操作使用同一个 Connection(事务一致性),且不同线程的 Connection 互不干扰。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 飞翔科技 —— 数据库连接绑定(ThreadLocal 实现线程级连接隔离)
 */
public class ConnectionHolder {

    // 模拟数据库连接(不依赖真实 JDBC 驱动)
    static class SimulatedConnection {
        private final int connectionId;
        private boolean closed = false;
        private boolean inTransaction = false;

        public SimulatedConnection(int connectionId) {
            this.connectionId = connectionId;
        }

        public void beginTransaction() {
            this.inTransaction = true;
            System.out.printf("  [连接#%d] 事务开始%n", connectionId);
        }

        public void commit() {
            this.inTransaction = false;
            System.out.printf("  [连接#%d] 事务提交 ✓%n", connectionId);
        }

        public void rollback() {
            this.inTransaction = false;
            System.out.printf("  [连接#%d] 事务回滚 ✗%n", connectionId);
        }

        public void close() {
            this.closed = true;
            System.out.printf("  [连接#%d] 连接关闭%n", connectionId);
        }

        public boolean isClosed() { return closed; }
        public int getConnectionId() { return connectionId; }
    }

    // 数据库连接管理器
    static class DataSourceManager {
        private static int nextId = 1;

        /**
         * 获取连接(真实场景从连接池获取)
         */
        public static SimulatedConnection getConnection() {
            return new SimulatedConnection(nextId++);
        }
    }

    /**
     * 连接持有者 —— 核心:用 ThreadLocal 绑定线程与连接
     */
    static class DbConnectionHolder {
        // JDK 8 风格:withInitial + Lambda
        private static final ThreadLocal&lt;SimulatedConnection&gt; connectionHolder =
                ThreadLocal.withInitial(() -> {
                    System.out.printf("  [Thread-%s] 初始化数据库连接%n",
                            Thread.currentThread().getName());
                    return DataSourceManager.getConnection();
                });

        /**
         * 获取当前线程绑定的连接
         */
        public static SimulatedConnection getConnection() {
            return connectionHolder.get();
        }

        /**
         * 释放当前线程的连接(必须调用!)
         */
        public static void releaseConnection() {
            SimulatedConnection conn = connectionHolder.get();
            if (conn != null && !conn.isClosed()) {
                conn.close();
            }
            connectionHolder.remove();  // 防止内存泄漏
        }
    }

    // 模拟业务 Service
    static class TransferService {
        public void transfer(long fromAccount, long toAccount, double amount) {
            System.out.printf("[%s] 开始转账: %d → %d, 金额: ¥%.2f%n",
                    Thread.currentThread().getName(), fromAccount, toAccount, amount);

            // 获取线程绑定的连接
            SimulatedConnection conn = DbConnectionHolder.getConnection();
            System.out.printf("[%s] 使用连接 #%d%n",
                    Thread.currentThread().getName(), conn.getConnectionId());

            try {
                conn.beginTransaction();

                // 模拟扣款
                System.out.printf("[%s] 账户 %d 扣款 ¥%.2f%n",
                        Thread.currentThread().getName(), fromAccount, amount);
                // 模拟加款
                System.out.printf("[%s] 账户 %d 入账 ¥%.2f%n",
                        Thread.currentThread().getName(), toAccount, amount);

                conn.commit();
                System.out.printf("[%s] 转账成功 ✓%n", Thread.currentThread().getName());
            } catch (Exception e) {
                conn.rollback();
                System.err.printf("[%s] 转账失败 ✗: %s%n",
                        Thread.currentThread().getName(), e.getMessage());
            } finally {
                // 注意:实际项目中在请求结束时统一释放,这里模拟
                DbConnectionHolder.releaseConnection();
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("===== 飞翔科技转账系统(ThreadLocal 连接绑定) =====\n");

        TransferService service = new TransferService();
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 并发执行 3 笔转账
        executor.execute(() -> service.transfer(10001, 10002, 5000.00));
        executor.execute(() -> service.transfer(20001, 20002, 3000.00));
        executor.execute(() -> service.transfer(30001, 30002, 1500.00));

        executor.shutdown();
        while (!executor.isTerminated()) {
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }

        System.out.println("\n===== 所有转账处理完毕 =====");
        System.out.println("每个线程独立持有自己的数据库连接,互不干扰。");
    }
}

控制台输出:

===== 飞翔科技转账系统(ThreadLocal 连接绑定) =====

[pool-1-thread-1] 开始转账: 10001 → 10002, 金额: ¥5000.00
  [Thread-pool-1-thread-1] 初始化数据库连接
[pool-1-thread-1] 使用连接 #1
[pool-1-thread-2] 开始转账: 20001 → 20002, 金额: ¥3000.00
  [Thread-pool-1-thread-2] 初始化数据库连接
[pool-1-thread-2] 使用连接 #2
  [连接#1] 事务开始
[pool-1-thread-1] 账户 10001 扣款 ¥5000.00
  [连接#2] 事务开始
[pool-1-thread-2] 账户 20001 扣款 ¥3000.00
[pool-1-thread-1] 账户 10002 入账 ¥5000.00
  [连接#1] 事务提交 ✓
[pool-1-thread-1] 转账成功 ✓
  [连接#1] 连接关闭
[pool-1-thread-2] 账户 20002 入账 ¥3000.00
  [连接#2] 事务提交 ✓
[pool-1-thread-2] 转账成功 ✓
  [连接#2] 连接关闭
[pool-1-thread-1] 开始转账: 30001 → 30002, 金额: ¥1500.00
  [Thread-pool-1-thread-1] 初始化数据库连接
[pool-1-thread-1] 使用连接 #3
  [连接#3] 事务开始
[pool-1-thread-1] 账户 30001 扣款 ¥1500.00
[pool-1-thread-1] 账户 30002 入账 ¥1500.00
  [连接#3] 事务提交 ✓
[pool-1-thread-1] 转账成功 ✓
  [连接#3] 连接关闭

===== 所有转账处理完毕 =====
每个线程独立持有自己的数据库连接,互不干扰。

六、线程池 + ThreadLocal 内存泄漏的完整复现与修复

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 飞翔科技 —— 线程池 + ThreadLocal 内存泄漏复现与修复
 */
public class ThreadLocalLeakDemo {

    // 模拟大对象(占用内存)
    static class BigObject {
        private final byte[] data = new byte[10 * 1024 * 1024]; // 10MB
        private final String name;

        public BigObject(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "BigObject{" + name + ", size=10MB}";
        }
    }

    // ThreadLocal —— 持有大对象引用
    static class LeakyContext {
        // 不带 withInitial,手动 set
        static final ThreadLocal&lt;BigObject&gt; DATA = new ThreadLocal<>();
    }

    // 安全的 ThreadLocal 使用模式
    static class SafeContext {
        static final ThreadLocal&lt;BigObject&gt; DATA = new ThreadLocal<>();

        static void set(BigObject obj) {
            DATA.set(obj);
        }

        static BigObject get() {
            return DATA.get();
        }

        static void clear() {
            DATA.remove();
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("===== 飞翔科技 ThreadLocal 内存泄漏复现 =====");
        System.out.println("(监控 JVM 堆内存,观察泄漏情况)\n");

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2, 2, 60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100)
        );

        // ========== 泄漏场景 ==========
        System.out.println("阶段1: 模拟泄漏(忘记 remove)");
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            pool.execute(() -> {
                // 设置大对象
                LeakyContext.DATA.set(new BigObject("Task-" + taskId));
                System.out.printf("[%s] 设置了 %s (未清理)%n",
                        Thread.currentThread().getName(),
                        LeakyContext.DATA.get());
                // ❌ 故意不调用 remove()!
            });
        }

        Thread.sleep(500);  // 等待任务执行完

        // 此时线程池中的线程仍然存活
        // ThreadLocalMap 中的 Entry:
        //   - key (ThreadLocal) → 弱引用,但 DATA 是 static final,不会被 GC
        //   - value (BigObject) → 强引用,不会被 GC
        // 如果 DATA 不是 static final,key 会被 GC 但 value 不会(泄漏)
        System.out.println("\n线程池线程仍在运行,ThreadLocalMap 中的大对象未被回收");
        System.out.println("如果忘记 remove,线程复用时旧数据残留\n");

        // 手动清理泄漏的数据(模拟正确做法)
        for (int i = 0; i < 2; i++) {
            pool.execute(() -> {
                LeakyContext.DATA.remove();  // 清理旧数据
                System.out.printf("[%s] 已清理残留的 ThreadLocal%n",
                        Thread.currentThread().getName());
            });
        }

        Thread.sleep(300);

        // ========== 安全场景 ==========
        System.out.println("\n阶段2: 安全使用(finally 中 remove)");
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            pool.execute(() -> {
                try {
                    SafeContext.set(new BigObject("SafeTask-" + taskId));
                    System.out.printf("[%s] 设置了 %s%n",
                            Thread.currentThread().getName(),
                            SafeContext.get());
                    // 执行业务逻辑...
                } finally {
                    SafeContext.clear();  // ✓ 保证清理
                    System.out.printf("[%s] 已安全清理%n",
                            Thread.currentThread().getName());
                }
            });
        }

        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);

        System.out.println("\n===== 结论 =====");
        System.out.println("1. 使用 ThreadLocal 后必须在 finally 中调用 remove()");
        System.out.println("2. 线程池场景下线程复用,不清理会导致数据残留和内存泄漏");
        System.out.println("3. JDK 8 的 withInitial() + remove() 是最佳实践");
    }
}

七、易错场景

7.1 线程池 + ThreadLocal 忘记 remove() —— 数据串号

// ❌ 严重 BUG:线程复用导致数据串号
ThreadLocal&lt;Integer&gt; userId = new ThreadLocal<>();
ExecutorService pool = Executors.newFixedThreadPool(2);

// 请求1: 用户大翔 (ID=1)
pool.execute(() -> {
    userId.set(1);  // 设置用户ID
    // ... 业务逻辑 ...
    // ❌ 忘记 remove()
});

// 请求2: 同一线程处理,读到上次残留的 ID=1!
pool.execute(() -> {
    Integer id = userId.get();  // 读到了 1!但应该是新用户!
    System.out.println("用户ID: " + id);  // 输出 1 —— 数据串号!
});

正确做法:拦截器中统一在 finally 块调用 remove()。

7.2 InheritableThreadLocal + 线程池导致值不可控

// ❌ 危险:线程池中 InheritableThreadLocal 的继承行为不可预测
InheritableThreadLocal&lt;String&gt; context = new InheritableThreadLocal<>();
context.set("大翔的权限");

ExecutorService pool = Executors.newFixedThreadPool(2);

// 父线程提交任务 → 子线程不一定能继承到值
// 因为线程池中的线程是复用的,不一定是"新建"的
pool.execute(() -> {
    System.out.println(context.get());  // 可能 null,也可能"大翔的权限"
});

// 正确做法:使用阿里开源的 TransmittableThreadLocal(TTL)
// 或手动在任务提交时传递上下文

7.3 JDK 8 withInitial() 的惰性初始化陷阱

// ❌ 误解:withInitial 不等于"自动初始化"
ThreadLocal&lt;Integer&gt; count = ThreadLocal.withInitial(() -> {
    System.out.println("初始化被调用");
    return 0;
});

// get() 时才触发初始化
System.out.println(count.get());  // 输出: 初始化被调用 → 0
// 第二次 get() 不会触发初始化
System.out.println(count.get());  // 输出: 0(直接返回值)
// 如果从未调用 get(),initialValue() 永远不会执行

7.4 ThreadLocal 变量声明为实例变量导致类加载器泄漏

// ❌ 危险:实例 ThreadLocal + 动态类加载 = 类加载器泄漏
public class Plugin {
    // 如果 Plugin 被动态加载/卸载
    private ThreadLocal<byte[]> cache = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
    
    // 线程仍持有 Plugin 的 ThreadLocal → Plugin.class 无法卸载 → 类加载器泄漏
}

正确做法:ThreadLocal 声明为 static,特别是 Web 容器中。


八、面试考点

Q1:ThreadLocal 的内存泄漏是如何发生的?(高频必问)

答:

  1. 引用结构:Thread → ThreadLocalMap → Entry[] → Entry(key=WeakRef<ThreadLocal>, value=Object)。
  2. 泄漏路径:当 ThreadLocal 外部强引用被清除后,key 因弱引用被 GC。但 value 通过 Thread → ThreadLocalMap → Entry 这条强引用链仍然可达,永远无法被 GC。
  3. 线程池加剧:线程池复用线程,线程存活时间 = 应用存活时间。只要线程不销毁,ThreadLocalMap 就一直存在,泄漏的 value 累积最终 OOM。
  4. JDK 的缓解措施:get()、set()、remove() 时会对 key 为 null 的 Entry(stale entry)进行探测式清理(expungeStaleEntry),但不是所有操作都触发清理。
  5. 根本解决:编码规范——每次使用后必须在 finally 中调用 remove()。

Q2:为什么 ThreadLocalMap 的 Entry 用弱引用?(高频)

答:

  • 如果 key 是强引用,只要线程不销毁,ThreadLocal 对象就永远无法被 GC。在使用线程池的场景下,这会导致 ThreadLocal 对象本身也泄漏。
  • 弱引用使得 ThreadLocal 在没有外部强引用时可以正常被 GC,key 变为 null,JDK 在后续操作中清理这些 stale entry。
  • 但弱引用只解决了 key 的回收问题,value 的泄漏仍需要 remove() 来解决。
  • 这是一种分而治之的设计:弱引用解决 key 生命周期问题,remove() 解决 value 生命周期问题。

Q3:InheritableThreadLocal 的原理是什么?

答:

  • 在 Thread.init() 方法中,创建子线程时会检查父线程的 inheritableThreadLocals。
  • 如果父线程的 inheritableThreadLocals 不为 null,则将其浅拷贝到子线程。
  • ThreadLocal.createInheritedMap() 创建一个新的 ThreadLocalMap,逐个复制 Entry。
  • 注意:复制发生在子线程创建时,之后父子线程的 ThreadLocal 值互不影响。
  • 线程池场景下,线程复用使得"创建时复制"的语义失效,需要 TransmittableThreadLocal(阿里开源)等方案。

Q4:JDK 8 中 ThreadLocal 有哪些改进?(JDK 8 专项)

答:

  1. withInitial(Supplier) 工厂方法:替代继承并重写 initialValue(),支持 Lambda 表达式。
    ThreadLocal&lt;Integer&gt; tl = ThreadLocal.withInitial(() -> 0);
    
  2. @Contended 注解支持:JDK 8 对 ThreadLocalRandom 的相关字段使用了 @Contended 消除伪共享,间接优化了 ThreadLocal 的随机数生成性能。
  3. ThreadLocal 本身在 JDK 8 中没有重大 API 变更,但 Lambda + withInitial 显著提升了代码简洁性。

九、ThreadLocal 最佳实践清单

#实践
1声明为 static final:防止类加载器泄漏,明确生命周期
2使用 withInitial():JDK 8 推荐,Lambda 一行搞定初始值
3务必在 finally 中 remove():线程池场景下这是铁律
4拦截器统一清理:Filter / Interceptor 的 finally 块是最佳清理点
5避免存放大对象:ThreadLocal 本质是"线程级全局变量",生命周期长
6线程池 + InheritableThreadLocal 不兼容:改用 TTL(TransmittableThreadLocal)
7监控 ThreadLocalMap 大小:通过反射或 JMX 监控,预警泄漏

上一篇:原子类详解  |  返回:章节导读

上一页
wait 与 notify 详解
下一页
原子类详解