ThreadLocal 全解析:从底层原理到实战场景,一篇搞懂

目录

什么是ThreadLocal

ThreadLocalMap内部结构

ThreadLocal的常用方法

存储数据void set(T value)

获取数据T get()

删除数据void remove()

为什么用ThreadLocal做key

实现线程隔离

唯一标识变量

配合弱引用设计

简化使用逻辑

为什么Thread不能作为ThreadLocalMap的Key来使用

Thread当Key

ThreadLocal当key

ThreadLocalMap如何查找数据

父子线程如何共享

ThreadLocal如何避免内存泄漏

threadLocal为什么会产生内存泄漏

如何避免内存泄漏

ThreadLcoal应用场景

线程数据隔离

跨函数传递

不用 ThreadLocal:靠参数 / 返回值传递,耦合度高

用 ThreadLocal:不用传参,解耦


什么是ThreadLocal

ThreadLocal被称为线程局部变量,用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程,所以该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

ThreadLocal用于在同一个线程间,在不同的类和方法之间共享数据的场景,也可以用于在不同线程间隔离数据的场景。

在 Java 中,每个 Thread 实例内部都维护着一个 ThreadLocalMap 对象(作为线程的私有属性),用于存储该线程的本地变量副本。ThreadLocal利用Thread中的ThreadLocalMap来进行数据存储

ThreadLocalMap内部结构

ThreadLocalMap内部数据结构是一个Entry类型的数组。每个Entry对象的Key为ThreadLocal对象,value为存储的数据

ThreadLocal的常用方法

存储数据void set(T value)

为当前线程单独存储一份变量副本,实现 “线程隔离”—— 不同线程通过同一个 ThreadLocal 对象存取值时,彼此互不干扰。

  public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果map已存在,就把当前ThreadLocal对象作为key,要存储的value作为值,存入map
        if (map != null) {
            map.set(this, value);
        } else {
        //如果 map 不存在(线程首次使用 ThreadLocal),则调用 createMap(t, value) 初始化 map 并存储数据。
            createMap(t, value);
        }
    }

获取数据T get()

get() 方法用于获取当前线程中与该 ThreadLocal 对象关联的变量值,如果不存在则返回初始值 。

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 尝试从 ThreadLocalMap 中获取 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果 ThreadLocalMap 为 null 或者没有找到对应的 Entry,返回初始值
    return setInitialValue(); 
}

删除数据void remove()

ThreadLocal 的 remove() 方法的作用是删除当前线程中与该 ThreadLocal 对象关联的变量值,及时调用 remove() 方法可以避免内存泄漏,因为 ThreadLocalMap 中 Entry 的键是对 ThreadLocal 对象的弱引用,如果不手动删除,在 ThreadLocal 对象被回收后,Entry 中的值依然会强引用实际对象,导致对象无法被回收,从而造成内存泄漏。

public void remove() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从 ThreadLocalMap 中移除当前 ThreadLocal 对应的键值对
        map.remove(this);
    }
}
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 断开对 ThreadLocal 对象的弱引用
            e.clear();
            // 清理过期的 Entry
            expungeStaleEntry(i);
            return;
        }
    }
}

为什么用ThreadLocal做key

实现线程隔离

  • ThreadLocal 的核心目的是为每个线程提供独立的变量副本, 实现线程之间变量的隔离。使用 ThreadLocal 作为 ThreadLocalMap 的 key,能清晰地标识每个线程中独立的变量。
  • 例如,在一个多线程的 Web 应用里,每个请求线程都可能需要存储各自的用户会话信息 。不同的 ThreadLocal 实例就像不同的 “标签”,当一个线程通过某个 ThreadLocal 实例调用 set 方法时,实际上是在以这个 ThreadLocal 实例为 key ,将对应的值存储到当前线程的 ThreadLocalMap 中。而其他线程通过相同或不同的 ThreadLocal 实例进行操作,访问的是各自线程 ThreadLocalMap 里独立的数据, 从而保证了线程之间的数据不会相互干扰。

唯一标识变量

  • 每个 ThreadLocal 实例都是唯一的。在程序中,可能会有多个不同用途的线程本地变量,使用 ThreadLocal 实例作为 key, 可以准确地区分不同的线程本地变量。
  • 比如,在一个复杂的多线程业务系统中,可能同时需要记录当前线程的请求 ID、用户权限等级等不同类型的线程本地信息。可以分别创建不同的 ThreadLocal 实例,如 requestIdThreadLocaluserPermissionThreadLocal , 每个实例作为独立的 key ,将对应的数据存储到线程的 ThreadLocalMap 中,方便后续根据不同的业务需求,准确获取对应的线程本地变量值。

配合弱引用设计

  • ThreadLocalMap 中 Entry 的 key 是对 ThreadLocal 的弱引用。这样设计的好处是,当 ThreadLocal 实例在外部没有强引用指向时,在垃圾回收过程中可以被回收。
  • 假设没有使用弱引用,并且 ThreadLocal 实例一直被 ThreadLocalMap 引用着,即使程序不再需要这个 ThreadLocal 实例,它也无法被回收,就会造成内存泄漏。而使用弱引用,当 ThreadLocal 实例失去外部强引用后,在下次垃圾回收时,ThreadLocalMap 中对应的 key 就会变为 null ,虽然此时 value 依然存在强引用,但在后续 setgetremove 等操作时,ThreadLocalMap 会对这种过期的 Entry 进行清理, 降低了内存泄漏的风险。

简化使用逻辑

  • 从代码使用的角度来看,使用 ThreadLocal 作为 key ,让 ThreadLocal 类自身的操作逻辑更加简洁直观。开发者只需要通过调用 ThreadLocal 实例的 setgetremove 等方法,就可以方便地操作线程本地变量, 无需额外管理复杂的 key 生成和存储逻辑。例如,ThreadLocal 的 set 方法内部会自动将当前 ThreadLocal 实例作为 key 进行存储操作,开发者不需要关心具体的 key 生成和 ThreadLocalMap 的底层细节,提升了代码的易用性和可读性。

为什么Thread不能作为ThreadLocalMap的Key来使用

Thread当Key

  1. 当线程中只有一个ThreadLocal对象时
    此时线程只需要存储 “1 个变量”,用Thread作为 key 确实能实现 “线程→变量” 的对应关系(每个线程一个值)。
    但这是极端场景,实际开发中几乎不会出现 —— 一个线程往往需要存储多种上下文信息(如用户 ID、请求 ID、事务状态等)。

  2. 当线程中有多个ThreadLocal对象时
    假设用Thread作为 key,那么所有变量都会被映射到同一个 key 上(当前线程实例),后存入的变量会覆盖先存入的变量,导致 “变量混乱”。

// 错误示例:用Thread作为key
Thread t = Thread.currentThread();
map.put(t, 用户信息);  // 存储用户信息
map.put(t, 请求ID);    // 覆盖用户信息(key相同)

此时无法同时保留 “用户信息” 和 “请求 ID”,失去了多变量存储的能力。

ThreadLocal当key

所以,不能使用Thread做key,而应该改用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get()方法,获取到当前线程的ThreadLocalMap,然后进一步获取到对应的Entry(Entry当中的key可以直接一一对应ThreadLocal的实例)

ThreadLocalMap如何查找数据

       当我们使用ThreadLocal获取当前线程中保存的Value数据时,是以ThreadLocal对象作为Key,通过访问Thread当前线程对象的内部ThreadLocalMap集合来获取到ValueThreadLocalMap集合的底层数据结构使用Entry[]数组保存键值对数据。所以,当通过ThreadLocal的get()、set()、remove()等方法,

int i = key.threadLocalHashCode & (table.length - 1);

通过key的 “hashCode值” 跟 “数组的长度减1” 做 “&按位与” 运算。其中key就是ThreadLocal对象。这种计算方式,相当于 “hashCode值” 跟 “数组的长度” 进行 “%取余” 运算,但是 “&按位与” 运算的效率更高

父子线程如何共享

在实际工作中,父线程中往ThreadLocal设置了值,在子线程中也能够获取到
 

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal=new ThreadLocal<>();
        threadLocal.set("肥嘟嘟左卫门");
        System.out.println("主线程main:"+threadLocal.get());

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程:"+threadLocal.get());
            }
        });
        thread.start();;
    }
}

运行结果:

主线程main:肥嘟嘟左卫门
子线程:null

在这种情况下使用ThreadLocal是行不通的。main方法在主线程中执行,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。两个线程,各自拥有不同的ThreadLocalMap,应该使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类

public class ThreadLocalTest2 {
    public static void main(String[] args) {
        InheritableThreadLocal threadLocal=new InheritableThreadLocal();
        threadLocal.set("肥嘟嘟左卫门");
        System.out.println("主线程main:"+threadLocal.get());
        
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程:"+threadLocal.get());
            }
        });
        thread.start();
    }
}

运行结果:

主线程main:肥嘟嘟左卫门
子线程:肥嘟嘟左卫门

ThreadLocal如何避免内存泄漏

threadLocal为什么会产生内存泄漏

ThreadLocalMap的内部数据结构Entry继承自WeakReference<ThreadLocal<?>>
这意味着Entry对ThreadLocal的引用是弱引用。根据弱引用的特性,当ThreadLocal对象没有其他强引用指向它时,在垃圾回收发生时,这个ThreadLocal对象就会被回收,此时Entry中key(ThreadLocal对象)就会变为null。但是,Entry中的value仍然被Entry对象强引用着,并且由于线程没有结束,ThreadLocalMap也不会被回收,这样value就一直无法被回收,从而造成了内存泄漏。

如何避免内存泄漏

使用完毕后,在finally调用ThreadLocal对象的remove()方法

以下是一个实际业务场景的示例(模拟用户登录后,在请求线程中存储用户信息,请求结束后清理):

import java.util.UUID;

public class ThreadLocalDemo {
    // 定义ThreadLocal,存储当前线程的用户ID(假设业务中需要在多个方法间共享用户信息)
    private static final ThreadLocal<String> CURRENT_USER_ID = new ThreadLocal<>();

    public static void main(String[] args) {
        // 模拟一个用户请求处理过程
        handleUserRequest("user_123");
    }

    /**
     * 处理用户请求的方法
     */
    public static void handleUserRequest(String userId) {
        try {
            // 1. 存储用户信息到ThreadLocal
            CURRENT_USER_ID.set(userId);
            System.out.println("请求开始,存储用户ID:" + userId);

            // 2. 模拟业务处理(可能调用多个工具类/服务,需要获取当前用户ID)
            doBusiness1();
            doBusiness2();

        } finally {
            // 3. 无论业务处理成功与否,最终都要清理ThreadLocal
            CURRENT_USER_ID.remove();
            System.out.println("请求结束,清理ThreadLocal,当前值:" + CURRENT_USER_ID.get()); // 清理后为null
        }
    }

    // 业务方法1:需要获取当前用户ID
    private static void doBusiness1() {
        String userId = CURRENT_USER_ID.get();
        System.out.println("业务1处理中,当前用户ID:" + userId);
    }

    // 业务方法2:需要获取当前用户ID
    private static void doBusiness2() {
        String userId = CURRENT_USER_ID.get();
        System.out.println("业务2处理中,当前用户ID:" + userId);
    }
}

remove()方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄漏问题

ThreadLcoal应用场景

线程数据隔离

ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,该线程对别的线程是不可见的,起到了隔离作用。在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能

比如 APP 里,10 个线程同时查 10 个用户的订单记录,每个线程要暂存 “当前查的用户 ID”,还得保证不串线(线程 A 的用户 ID 不能被线程 B 改了)。

用 ThreadLocal 就特简单:

// 1. 造个ThreadLocal,存当前线程的用户ID
static ThreadLocal<String> currUserId = new ThreadLocal<>();

public static void main(String[] args) {
    // 2. 开3个线程,分别查用户1、2、3的订单
    new Thread(() -> {
        currUserId.set("用户1"); // 线程1存自己的用户ID
        getOrder(); // 查订单时直接取,不用传参数
        currUserId.remove(); // 用完清掉
    }, "线程1").start();

    new Thread(() -> {
        currUserId.set("用户2"); // 线程2存自己的
        getOrder();
        currUserId.remove();
    }, "线程2").start();
}

// 查订单的方法
static void getOrder() {
    // 直接取当前线程的用户ID,不用传参,还不会和其他线程串
    String userId = currUserId.get();
    System.out.println(Thread.currentThread().getName() + "查:" + userId + "的订单");
}

线程 1 查用户 1,线程 2 查用户 2,互不干扰。

要是不用 ThreadLocal,要么得给getOrder()传参数(多层调用会很麻烦),要么用锁(多线程排队,慢)——ThreadLocal 既不用传参,又不用等锁,这就是它的好~

跨函数传递

数据通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度

举个栗子🌰🌰🌰:

假设一个简单的电商下单场景:从 “接收请求” 到 “创建订单”,要经过 3 个类(OrderControllerOrderServiceOrderDAO),每个环节都需要 “当前用户 ID”。

不用 ThreadLocal:靠参数 / 返回值传递,耦合度高

// 1. 控制器:接收请求,拿到用户ID,传给Service
class OrderController {
    private OrderService orderService = new OrderService();
    
    // 下单接口:必须把userId作为参数传给service
    public void createOrder(String userId, String goodsId) {
        System.out.println("控制器拿到用户ID:" + userId);
        orderService.doCreateOrder(userId, goodsId); // 传userId
    }
}

// 2. 服务层:拿到userId,再传给DAO
class OrderService {
    private OrderDAO orderDAO = new OrderDAO();
    
    // 必须接收userId参数,再往下传
    public void doCreateOrder(String userId, String goodsId) {
        System.out.println("服务层拿到用户ID:" + userId);
        // 还可能调用其他工具类(比如日志、权限校验),也要传userId
        LogUtils.log(userId, "开始创建订单");
        orderDAO.saveOrder(userId, goodsId); // 传userId
    }
}

// 3. DAO层:最终用userId存订单
class OrderDAO {
    // 必须接收userId参数
    public void saveOrder(String userId, String goodsId) {
        System.out.println("DAO层用用户ID存订单:" + userId + "-" + goodsId);
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        OrderController controller = new OrderController();
        controller.createOrder("用户123", "商品456");
    }
}

问题:耦合度高

  • 每个方法都要加userId参数,哪怕方法本身不 “直接需要” 这个参数(比如OrderService的核心逻辑是处理订单,却必须传userId给 DAO);
  • 要是后续需要加新参数(比如 “用户等级”),controller→service→DAO的所有方法都要改参数,牵一发动全身。

用 ThreadLocal:不用传参,解耦

// 1. 用ThreadLocal存当前线程的用户ID(全局工具类)
class UserContext {
    public static ThreadLocal<String> currUserId = new ThreadLocal<>();
}

// 2. 控制器:存用户ID,不用传参给Service
class OrderController {
    private OrderService orderService = new OrderService();
    
    public void createOrder(String userId, String goodsId) {
        UserContext.currUserId.set(userId); // 存到ThreadLocal
        System.out.println("控制器存用户ID:" + userId);
        orderService.doCreateOrder(goodsId); // 不用传userId了!
        UserContext.currUserId.remove(); // 用完清理
    }
}

// 3. 服务层:直接取,不用接收参数,也不用传DAO
class OrderService {
    private OrderDAO orderDAO = new OrderDAO();
    
    // 方法参数里没有userId了!
    public void doCreateOrder(String goodsId) {
        String userId = UserContext.currUserId.get(); // 直接取
        System.out.println("服务层拿到用户ID:" + userId);
        LogUtils.log(userId, "开始创建订单"); // 工具类也直接取
        orderDAO.saveOrder(goodsId); // 不用传userId!
    }
}

// 4. DAO层:直接取,不用接收参数
class OrderDAO {
    public void saveOrder(String goodsId) {
        String userId = UserContext.currUserId.get(); // 直接取
        System.out.println("DAO层用用户ID存订单:" + userId + "-" + goodsId);
    }
}

优势:解耦

  • 方法不用再带 “传递用” 的参数(userId),每个类 / 方法只关注自己的核心逻辑(控制器管接收请求、服务层管业务、DAO 管存数据);
  • 后续加新参数(比如 “用户等级”),只需在UserContext加新的 ThreadLocal,不用改所有方法的参数列表,改动极小。

简单说:不用 ThreadLocal,参数像 “接力棒” 一样在类 / 方法间传;用了 ThreadLocal,参数像 “自己的口袋”,每个线程自己存、自己取,不用麻烦别人传,类和方法之间就松绑了~

以上从 ThreadLocal 的核心定义、底层结构,到常用方法、关键设计逻辑(如为何用 ThreadLocal 做 key),再到实际应用场景(线程隔离、跨方法传参)与内存泄漏解决方案,完整梳理了 ThreadLocal 的核心知识点。它看似简单,却是多线程开发中 “解耦” 与 “提效” 的关键工具 —— 用好 ThreadLocal 能避免冗余参数传递和锁竞争,用错则可能引发内存泄漏。希望这份总结能帮你真正理解并合理运用 ThreadLocal,在实际开发中少踩坑、高效解决线程数据管理问题~

双节将至,秋意正浓!小编提前送上祝福:愿大家国庆玩得尽兴,中秋吃得香甜,日子越过越圆满,平安喜乐常相伴~ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值