ThreadLocal

ThreadLocal 概述

什么是 ThreadLocal,用于解决什么问题?用在什么场景合适?

先来看看官方文档的描述:

[!quote]
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

该类提供了 线程局部变量,这些变量和普通变量不一样,当每个线程访问自己的这个变量的时候,访问到的都是独属于自己的变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

按我自己的话说:

[!note]
ThreadLocal 是一个将在多线程中为每一个线程创建单独的变量副本的类,每个线程在访问它的时候,访问到的都是自己的那一份副本,把他设置成 private static 的话,在当前线程内就都能访问到,可以用来存储和当前线程相关的一些状态,并且避免多线程因为操作共享变量导致的数据不一致

解决线程安全的思路:

因此,ThreadLocal 很适合用于保证线程安全,或者存储一些和线程绑定的数据。

快速开始

提到 ThreadLocal 被提到应用最多的是 session 管理和数据库链接管理,这里以数据库连接访问为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
@Override protected Connection initialValue() {
try {
return DriverManager.getConnection("", "", "");
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
};

public Connection getConnection() { return dbConnectionLocal.get(); } }

这样每个线程调用 ConnectionManagergetConnection 的时候,拿到的都是属于自己的连接,如果不用 ThreadLocal 的话,那么就可能导致同一个连接被多个线程使用,造成各种问题。

不过一般用连接池管理,而不是 ThreadLocal

ThreadLocal 原理

ThreadLocalMap

Thread 类中,有这么一个字段 threadLocals

用来存储所有 ThreadLocal 变量,以 Map 的形式,Key 是 ThreadLocal 变量的 HashCode,Value 就是 ThreadLocal 存储的内容。

但这个 Map 结构有些不一样:

  • 它没有实现 Map 接口;
  • 它没有 public 的方法, 最多有一个 default 的构造方法, 因为这个 ThreadLocalMap 的方法仅仅在 ThreadLocal 类中调用, 属于静态内部类
  • ThreadLocalMap 的 Entry 实现继承了WeakReference<ThreadLocal<?>>
  • 该方法仅仅用了一个 Entry 数组来存储 Key, Value; Entry 并不是链表形式, 而是每个 bucket 里面仅仅放一个 Entry;

ThreadLocal.set

该方法的步骤:

  1. 使用 Key 的 HashCode 对 Entry 数据取余,相当于一个哈希的过程,获取存储的位置
  2. 然后从哈希出的位置开始 线性探测,如果找到了那么设置为当前最新的值返回
  3. 在线程探测的过程中,如果发现有的 key 是 null,那么说明这个 ThreadLocal 被清理了,则将该 Entry 设置为当前插入的 Value返回
  4. 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value。然后将哈希表扩容,如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容

ThreadLocal.get

其实很简单,就是去哈希表找,找不到则用线性探测法继续往后找

get 原理

当调用 ThreadLocal 变量的 get() 方法的时候:

会首先获取当前的线程,然后以当前线程为参数,去获取 ThreadLocal 的值

具体步骤:

  1. 通过当前线程拿到 Thread 类中的 map
  2. 如果 map 不为空,则以 ThreadLocal 变量本身为 Key 获取存储的 Value,并返回
  3. 如果 map 为空,调用 setInitialValue 方法返回

setInitialValue 方法如下:

  • 首先调用 initialValue 方法, 产生一个 Value 对象
  • 继续查看当前线程的 threadLocals 是不是空的, 如果 ThreadLocalMap 已被初始化, 那么直接将产生的对象添加到 ThreadLocalMap 中, 如果没有初始化, 则创建并添加对象到其中;

注意:创建 ThreadLocal 的时候是可以重载 initialValue 方法的

set 原理

当调用 ThreadLocal 的 set(T value) 方法去设置值的时候,也会先去获取当前线程,然后以当前线程为参数来设置 ThreadLocal 的值

具体步骤:

  • 根据当前线程获取 map
  • 如果 map 不为空,则直接以 ThreadLocal 变量为 Key,set 的值为 Value 插入 map
  • 如果 mpa 为空,则创建 map 同时插入 Key,Value

ThreadLocal 内存泄漏

为什么 ThreadLocal 可能会导致内存泄漏呢?看以下场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal();
local.set(new Test());
local = null;
// 手动触发GC,此时ThreadLocal被回收,那么value是否被回收呢?
System.gc();
// GC是异步执行的,主线程Sleep一会,等待对象回收
ThreadUtil.sleep(1000);
}

// 对象被回收时触发
@Override protected void finalize() throws Throwable {
System.err.println("对象被回收...");
}
}

在这种场景下,当程序运行完成后,由于 ThreadLocal 是局部变量,运行完后,不再持有 ThreadLocal 的强引用,而在 ThreadLocalMap 中的 Entry 里,ThreadLocal 作为 Key 是弱引用,没有其他强引用指向,所以会被 GC 回收掉。但是 Value 被 Entry 强引用,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

ThreadLocalMapgetset 方法会清除线程ThreadLocalMap里所有keynullvalue,可以作为一层预防措施。

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用线程池的时候,这个线程执行任务结束,ThreadLocal对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。
  • 使用线程池的时候,线程执行任务结束,但是 ThreadLocalprivate static 的,导致强引用一直存在,虽然这些数据已经不用了,但是还在内存中
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么这个期间就会发生内存泄漏。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

==怎么防止 ThreadLocal 内存泄漏呢? 使用完后及时 remove==

ThreadLocal 使用场景

  • Web中的 Session 管理
  • 微服务架构中,跨服务调用时的跟踪ID(如分布式链路追踪中的traceId)可以通过 ThreadLocal 在线程内传递,确保所有日志记录都能包含这一标识,便于后续的日志分析和问题排查。
  • 当每个线程需要独立拥有一个对象实例(如工具类或资源)以避免状态冲突时,使用 ThreadLocal 可以为每个线程提供独立的实例副本

参考


ThreadLocal
http://potatotato.github.io/2024/06/23/ThreadLocal/
作者
发布于
2024年6月23日
许可协议