TreadLocal和TreadLocalMap

文章目录


一、ThreadLocal

在多线程环境下,多个线程访问同一个共享变量时,容易出现线程安全问题。我们通常用synchronized或Lock来加锁解决,但加锁会让线程排队执行,降低并发效率。

ThreadLocal提供了另一种思路:为每个线程创建独立的变量副本,每个线程操作自己的那份副本,互不干扰。这样既避免了线程安全问题,又无需加锁,性能更高。

  • set(T value):在当前线程中设置一个值。
  • get():获取当前线程中存储的值。
  • remove():移除当前线程中存储的值,防止内存泄漏(非常重要) 。
java 复制代码
class ThreadLocalDemo {
    // 声明一个ThreadLocal变量(建议用static final修饰)
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();

    public static void main(String[] args) {
        // 线程1:存入"张三"
        new Thread(() -> {
            threadLocal.set("张三");
            threadLocal1.set("aaa");
            System.out.println("线程1获取:" + threadLocal.get());  // 张三
            System.out.println("线程1获取:" + threadLocal1.get());  // aaa
            threadLocal.remove();  // 用完清理
        }, "线程1").start();

        // 线程2:存入"李四"
        new Thread(() -> {
            threadLocal.set("李四");
            System.out.println("线程2获取:" + threadLocal.get());  // 李四
            threadLocal.remove();
        }, "线程2").start();

        // 主线程:没有存过,获取为null
        System.out.println("主线程获取:" + threadLocal.get());  // null
    }
}

输出结果:线程1打印"张三",线程1的threadLocal1打印"aaa",线程2打印"李四",主线程打印"null"------三个线程互不影响。

二、ThreadLocal的线程隔离

ThreadLocal线程隔离核心:每个线程Thread对象内部,都持有一个ThreadLocalMap

  • Thread 对象持有 ThreadLocal.ThreadLocalMap 类型的成员变量
  • ThreadLocalMap 内部是一个 Entry[] 数组
  • Entry 继承自 WeakReference<ThreadLocal<?>>,key 是对 ThreadLocal 实例的弱引用,value 是存入的数据(强引用)


图片来源

1.set方法源码

set 方法先拿到当前线程的引用 Thread.currentThread(),通过 getMap(t) 获取该线程内部的 threadLocals(即 ThreadLocalMap)。如果 Map 已存在,以当前 ThreadLocal 实例为 key,将 value 存入。如果 Map 不存在(线程第一次使用 ThreadLocal),则创建一个新的 ThreadLocalMap 并存入数据。

java 复制代码
// ThreadLocal.set() 简化源码
public void set(T value) {
    Thread t = Thread.currentThread();// 1. 获取当前线程
    ThreadLocalMap map = getMap(t);   // 2. 获取当前线程的ThreadLocalMap
    if (map != null)
        map.set(this, value);         // 3. 以当前ThreadLocal为key存值
    else
        createMap(t, value);          // 4. 首次创建ThreadLocalMap
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;            // 直接返回Thread对象的成员变量
}

数据实际存储在线程Thread对象的threadLocals中,ThreadLocal本身只是一个key

2.get方法源码

get方法会先获取当前线程。然后获取当前线程的 ThreadLocalMap。以当前 ThreadLocal 实例为 key,查找对应的 Entry。找到则返回 value。如果 Map 为 null 或 Entry 为 null,调用 setInitialValue() 初始化默认值(默认返回 null,可重写 initialValue() 方法)

java 复制代码
// ThreadLocal.get() 简化源码
public T get() {
    Thread t = Thread.currentThread();  // 1. 获取当前线程
    ThreadLocalMap map = getMap(t);     // 2. 获取当前线程的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 3. 以this为key查找Entry
        if (e != null) {
            return (T) e.value;     // 4. 返回value
        }
    }
    return setInitialValue();       // 5. Map未初始化则设置初始值
}

三、ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的静态内部类,是一个自定义的哈希表,专门用来存储线程局部变量。它的 key 是 ThreadLocal 实例,value 是线程存储的数据。

ThreadLocalMap 的 Entry 继承自 WeakReference<ThreadLocal<?>>

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** 存入的值(强引用) */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);        // key是弱引用
        value = v;       // value是强引用
    }
}
  • key 使用弱引用,如果 key 是强引用,即使我们在代码中把 ThreadLocal 对象设为 null,ThreadLocalMap 中的 Entry 仍然强引用了它,导致它无法被GC回收,从而造成内存泄漏。

  • 使用弱引用后,当 ThreadLocal 对象除了弱引用外再无其他强引用时,GC 会回收它,Entry 的 key 变为 null

1.ThreadLocalMap的哈希冲突

在 ThreadLocal 中采取的是开放地址法的方法来解决哈希冲突。当遇到哈希冲突时,会再次进行线性探测,探测的意思其实就是去寻找下一个空位。

发生哈希冲突时:

  1. 先计算目标位置 index = key.threadLocalHashCode & (len - 1)
  2. 如果该位置已被占用
    ├── key 相同 → 直接覆盖
    └── key 不同 → 向后查找下一个位置(index+1, index+2, ...)
  3. 直到找到空位或相同的 key

2.过期Key的探测式清理

ThreadLocal 的设计者意识到了内存泄漏的风险,在 set、get 等方法中内置了探测式清理机制。当发现 key 为 null 的过期 Entry 时,会主动将其 value 置为 null,帮助 GC 回收。

java 复制代码
// expungeStaleEntry(i) 的核心逻辑(简化)
private int expungeStaleEntry(int staleSlot) {
    // 1. 将当前位置的 value 置为 null,Entry 置为 null
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    
    // 2. 向后遍历,检查并清理其他过期 Entry
    // 3. 对未过期的 Entry 进行 rehash,填补空位
    // ...
}

四、ThreadLocalg内存泄漏问题

内存泄漏是指程序中不再使用的对象无法被 GC 回收,始终占用内存,久而久之可能导致 OutOfMemoryError。

当 ThreadLocal 对象的外部强引用断开(比如生命周期结束或被手动置为 null)时,GC 会回收它。此时 Entry 的 key 变成 null,但 value 仍然被 Entry 强引用,无法被回收。此时

  • 如果线程随之结束:Thread 对象被回收,ThreadLocalMap 也被回收,value 自然释放,没问题。
  • 如果线程长期存活(如线程池) :ThreadLocalMap 会一直存在,value 堆积越来越多,最终导致内存泄漏甚至 OOM。

最直接的办法就是使用完ThreadLocal后,在finally块中手动调用remove()方法。remove() 方法会主动将 ThreadLocalMap 中该 Entry 的 key 和 value 都清除。

java 复制代码
// 正确的使用姿势
public class ThreadLocalBestPractice {
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

    public void process(User user) {
        userHolder.set(user);
        try {
            // 执行业务逻辑,通过 userHolder.get() 获取当前线程的用户对象
        } finally {
            userHolder.remove();  // ⚠️ 一定要在 finally 中清理!
        }
    }
}

五、InheritableThreadLocal:父子线程数据传递

普通的 ThreadLocal 无法在父子线程间传递数据:主线程存的值,子线程拿不到。InheritableThreadLocal 解决了这个问题,在创建子线程的瞬间,它将父线程的本地变量复制给子线程。

  • main 线程是父线程,new Thread 创建的是子线程。
java 复制代码
public class InheritableThreadLocalDemo {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static final InheritableThreadLocal<String> inheritableTL = 
        new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("父线程-threadLocal的值");
        inheritableTL.set("父线程-inheritableTL的值");

        new Thread(() -> {
            // ThreadLocal:拿不到父线程的数据
            System.out.println("子线程获取ThreadLocal:" + threadLocal.get());
            // InheritableThreadLocal:可以拿到
            System.out.println("子线程获取Inheritable:" + inheritableTL.get());
        }).start();
    }
}

实际应用场景

1.数据库连接管理

每个线程使用自己的 Connection,避免事务混乱和连接竞争

java 复制代码
public class ConnectionHolder {
    private static final ThreadLocal<Connection> connHolder = new ThreadLocal<>();

    public static Connection getConnection() {
        Connection conn = connHolder.get();
        if (conn == null) {
            conn = DriverManager.getConnection(url, user, password);
            connHolder.set(conn);
        }
        return conn;
    }

    public static void closeConnection() {
        Connection conn = connHolder.get();
        if (conn != null) {
            conn.close();
            connHolder.remove();
        }
    }
}

2.用户会话信息存储

在Web应用中,将当前请求的用户信息存入ThreadLocal,整个请求链路无需显式传递用户对象

java 复制代码
public class UserContext {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

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

    public static User getUser() {
        return currentUser.get();
    }

    public static void clear() {
        currentUser.remove();
    }
}

面试问题

1.一个线程可以有多个ThreadLocal对象吗?

  • 可以。线程内部的ThreadLocalMap是一个数组,不同的ThreadLocal实例通过哈希计算定位到数组的不同位置,所以一个线程可以持有多个ThreadLocal对象

2.ThreadLocalMap的key是如何计算出来的?

  • 每个 ThreadLocal 实例在创建时,都会从静态的 AtomicInteger 计数器中获取一个唯一的 threadLocalHashCode。这个值不是简单的自增,而是每次加上一个黄金分割数(0x61c88647),目的是让哈希值分布更加均匀,减少冲突。
  • 计算数组下标的公式:
java 复制代码
int i = key.threadLocalHashCode & (table.length - 1);
  • 这里 table.length 总是 2 的幂,所以 (table.length - 1) 相当于取低几位,和 HashMap 的索引计算原理一致。

3.ThreadLocal的get()方法执行时,发生GC后key是否为null?

  • 如果外部强引用被回收,那么发生GC后,Entry的key(弱引用)会被置为null,但value仍然存在。此时调用get()方法,ThreadLocalMap会通过探测式清理机制跳过key为null的Entry,然后返回setInitialValue()的默认值。

4.ThreadLocal的set()和get()方法源码中用到了什么设计模式?

  • initialValue()方法是一个protected方法,默认返回null,交给子类去重写,这是典型的模板方法模式。spring的RequestContextHolder等框架大量使用了这种模式【这里不太懂,用通俗的语言解释】

5.InheritableThreadLocal是什么?和ThreadLocal有何区别?

  • InheritableThreadLocal继承自ThreadLocal,可以在创建子线程时自动将父线程的本地变量传递给子线程。区别在于:ThreadLocal操作threadLocals,InheritableThreadLocal操作inheritableThreadLocals。传递只在子线程创建时发生一次,线程池环境下不适用。
相关推荐
直奔標竿1 小时前
Java开发者AI转型第二十三课!Spring AI个人知识库实战(二):异步ETL流水线搭建与避坑指南
java·人工智能·spring boot·后端·spring
CyL_Cly1 小时前
localsend安卓手机下载 支持win/mac/ubuntu
android·macos·智能手机
AC赳赳老秦1 小时前
网安工程师提效:用 OpenClaw 实现漏洞扫描报告生成、安全巡检自动化、日志合规审计
java·开发语言·前端·javascript·python·deepseek·openclaw
大尚来也1 小时前
防御现代Web威胁:使用PHP原生过滤器防止SQL注入与XSS的终极指南
android
ffqws_1 小时前
MyBatis 动态 SQL 详解:从原理到实战
java·sql·mybatis
浮尘笔记1 小时前
在Snowy后台无需编码实现自动化生成CRUD操作流程
java·开发语言·经验分享·spring boot·后端·程序人生·mybatis
-星空下无敌1 小时前
IDEA 2025.3.1最新最全下载、安装、配置及使用教程(保姆级教程)
java·ide·intellij-idea
JAVA面经实录9171 小时前
Spring Boot + Spring AI 一体化实战全文档
java·人工智能·spring boot·spring
idealzouhu1 小时前
【NDK开发】Android NDK 原生构建:ndk-build 与 CMake
android·ndk