【一文读懂】你也要用ThreadLocal吗?ThreadLocal源码解析

2025-06-23 12:14:04 今晚世界杯

🐻大家好,我是木木熊

🌍️公众号:「程序员木木熊 」

本文以学习交流和分享为目的,如有不正确的地方,欢迎大家批评指正!!

前“戏”

一直996写代码的猿猿/媛媛们,你们是否经常出现,以下症状:

线上问题找不到日志,日志无法串联,问题定位困难

方法调用链路长,数据传递不畅

长期使用SimpleDateFormat,导致时间混乱

分页插件PageHelper问题频发

长此以往,心力交瘁,难以下班。现在只需要阅读此文,你就能轻松掌握:

ThreadLocal的实现原理

轻松解决多线程环境下的数据隔离问题

优雅的实现方法间上下文传递

自定义TraceId,查日志不再困难

突然戏精上身,hhh。

MOVE回来,本文将介绍一下ThreadLocald常用的使用场景,通过源码解析原理,展示具体代码示例,总结实践指南。

大家以后面试遇到ThreadLocal题,再也不用打面试官啦!

ThreadLocal常见的使用场景

1.解决线程安全问题

对于一些线程不安全的类,如SimpleDateFormat,多线程场景下,使用ThreadLocal为每个线程维护一个独立的副本,避免出现线程安全的问题。

2.上下文信息传递

接口请求链路中,一些上下文信息(如用户信息)常常需要在方法间一直传递。可以把这些信息放在ThreadLocal中,而不必将它们作为方法的参数逐层传递,代码实现会更加优雅。

3.Spring事务实现

Spring通过@Transactional注解来实现数据库事务,为了保证所有的操作都是在同一个连接上完成的,使用ThreadLocal来存储数据库连接Connection对象。

4.PageHelper分页信息传递

PageHelper.startPage方法,将分页信息存储在ThreadLocal常量LOCAL_PAGE中,分页拦截器PageInterceptor基于LOCAL_PAGE判断是否执行分页和组装分页SQL,执行完成后清除LOCAL_PAGE。

5.MDC记录TraceId

为了串联一个请求的日志,通常会为请求设置TraceId,一般通过MDC来添加TraceId,并打印到日志中,而MDC底层实现也是使用的ThreadLocal。

ThreadLocal源码解析

1.如何初始化

ThreadLocal构造方法为空,没有任何逻辑

// 构造方法

public ThreadLocal() {

}

进行set方法或get方法调用时,如果判断当前线程中threadLocals变量为空,就会调用createMap方法完成初始化。

createMap方法的逻辑也比较简单,即为当前线程成员变量threadLocals赋值。

void createMap(Thread t, T firstValue) {

//初始化ThreadLocalMap

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

2.核心类ThreadLocalMap

ThreadLocalMap是ThreadLocal的一个静态内部类,其本质就是一个哈希表。

哈希表的key为ThreadLocal,用来为某一个线程,存储不同类型的ThreadLocal值。

哈希表的value为使用者设置的具体对象值。

每个线程都独立持有自己的ThreadLocalMap,即成员变量threadLocals,通过上文提到的createMap方法进行初始化。

//ThreadLocalMap

static class ThreadLocalMap {

private static final int INITIAL_CAPACITY = 16;

//Hash表头数组

private Entry[] table;

private int size = 0;

private int threshold;

...

//map的set方法

private void set(ThreadLocal key, Object value {

Entry[] tab = table;

int len = tab.length;

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

...

}

//map的get方法

private Entry getEntry(ThreadLocal key) {

int i = key.threadLocalHashCode

& (table.length - 1);

Entry e = table[i];

...

}

}

这里有两点需要注意

ThreadLocalMap是存储在Thread中的,即成员变量threadLocals

ThreadLocalMap的key不是线程对象,而是所使用的ThradLocal对象,即方法中的this

这两点是实现ThreadLocal线程隔离和ThradLocalMap随线程销毁而销毁的关键。

ThreadLocalMap的静态内部类Entry继承了WeakReference(弱引用),若一个对象只被弱引用所引用,那么它将在下一次GC中被回收掉。后续当写一篇JAVA中四种引用的区别。

// Entry继承WeakReference

static class Entry extends WeakReference> {

Object value;

Entry(ThreadLocal k, Object v) {

super(k);

value = v;

}

}

ThreadLocal使用弱引用是为了避免发生内存泄漏,是ThreadLocal的保护机制。

3.设值,取值和清空

ThreadLocal为了实现线程隔离,所有的操作,其实底层都是基于每个线程自己独有的ThreadLocalMap进行操作,下面的set/get/remove操作,实际都是基于ThreadLocalMap底层的方法进行的。

设值-set方法,方法在进行set操作时,会有一个对Entry中key的判断,如果为null,会进行清除。在后面的取值set方法和清除remove方法中也有类似的操作,这也是ThreadLocal应对内存泄漏的保护机制。

//ThreadLocal的set方法

public void set(T value) {

// 获取当前线程

Thread t = Thread.currentThread();

// 获取当前线程的 ThreadLocalMap

ThreadLocalMap map = getMap(t);

if (map != null) {

map.set(this, value);

} else {

//当前线程的ThreadLocalMap初始化

createMap(t, value);

}

}

//ThreadLocalMap的set方法

private void set(ThreadLocal key, Object value {

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)]) {

ThreadLocal k = e.get();

// k == key 把值设置到Entry的value中

if (k == key) {

e.value = value;

return;

}

// 这里的操作是在ThreadLocal被回收是,避免产生内存泄漏

if (k == null) {

//清除Key为null的数据

replaceStaleEntry(key, value, i);

return;

}

}

}

取值-get方法,这里有一段逻辑getEntryAfterMiss,也是对key为null的数据进行清除操作。

// ThreadLocal的get方法

public T get() {

Thread t = Thread.currentThread();

//获取当前线程的ThreadLocalMap

ThreadLocalMap map = getMap(t);

if (map != null) {

//ThreadLocalMap的getEntry方法进行取值

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null) {

@SuppressWarnings("unchecked")

T result = (T)e.value;

return result;

}

}

//map为null时,初始化map,并最终初始化value值为null

return setInitialValue();

}

//ThreadLocalMap的getEntry方法

private Entry getEntry(ThreadLocal key) {

//正常的取值操作

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

Entry e = table[i];

if (e != null && e.get() == key)

return e;

else

//未取到值时,进行后续清理操作等

return getEntryAfterMiss(key, i, e);

}

清理-remove方法,Entry的clear方法,把弱引用置为null,后续的expungeStaleEntry方法同上面一样,也是对key值为null的数据进行清理,防止内存泄漏。

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) {

//去掉弱应用的引用

e.clear();

expungeStaleEntry(i);

return;

}

}

}

//Reference类的clear操作

public void clear() {

this.referent = null;

}

4.ThreadLocal内存泄漏分析

下图大致描绘了ThreadLocal的对象内存关系(可能不够严谨)

先说一下ThreadLocal导致内存泄漏需要满足的三个条件:

ThreadLocal被回收,即使用完后,且只存在弱引用时被GC

线程被复用,如线程池

未再调用set/get/remove方法

我们来描述一下这个过程:

在方法中new了一个局部变量ThreadLocal对象,使用完后,跳出方法,相当于失去强引用。后续GC中,因为只存在弱引用key,改对象会被回收。

ThreadLocal被回收,但是因为Thread线程复用,依然持有对ThreadLocalMap的引用,之前ThradLocal对应的Entry依然被引用,只是它的key已经变成null,value还是之前的值,这些key为null的Entry节点的value无法被访问。

如果线程迟迟不结束或者一直被复用,那么这些value会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value永远不会回收。

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

因此,使用ThreadLocal,谨记使用完后一定要进行remove操作,进行清除,避免内存泄漏的发生。并且如果不及时清理,除了内存泄漏,更严重的是导致业务数据的错乱,进而出现莫名奇妙的bug。

ThreadLocal代码实践

1.存储用户登录信息,上下文传递

通常配合鉴权切面使用,在通过token获取到用户信息后,存储到ThreadLocal中,后续只需要通过ThreadLocal静态常量就可以方便获取登录用户信息,而不需要在方法和类之间传递。具体实现如下

LoginUserContext类,持有ThreadLocal常量,并提供静态get/set/clear方法,方便调用

// 持有登录信息的类

public class LoginUserContext {

// ThreadLocal静态常量

public static final ThreadLocal LOCAL_USER = new ThreadLocal<>();

//设置用户信息

public static void setUser(LoginUser loginUser) {

LOCAL_USER.set(loginUser);

}

//获取用户信息

public static LoginUser getUser() {

return LOCAL_USER.get();

}

//清除用户信息

public static void clear() {

LOCAL_USER.remove();

}

}

鉴权切面,执行完成后finally中清除登录用户信息

@Aspect

@Component

public class PermissionAspect {

// 对所有的Controller进行拦截

@Pointcut("execution(* *..*Controller.*(..))")

public void controllerAspect() {

}

@Around("controllerAspect()")

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

Object ret = null;

try {

//获取登录用户信息

LoginUser loginUser = getLoginUser();

if (loginUser == null) {

throw new RuntimeException("token已过期或未登录");

}

//用户信息设置到ThreadLocal

LoginUserContext.setUser(loginUser);

//执行业务逻辑

ret = joinPoint.proceed();

} finally {

//清理用户信息ThreadLocal

LoginUserContext.clear();

}

return ret;

}

//根据token获取登录用户信息

private LoginUser getLoginUser() {

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

String accessToken = request.getHeader("token");

if (StringUtils.isBlank(accessToken)) {

return null;

}

//...此处逻辑省略

return new LoginUser();

}

}

业务方法在调用时,只需要通过LoginUserContext的静态方法就可以获取到登录用户信息

@Service

public class OrderLogic {

public void createOrder(){

//获取用户信息

LoginUser loginUser = LoginUserContext.getUser();

//业务逻辑...

}

}

2.MDC存储TraceId,串联日志

MDC底层实现是一个记录Map的ThreadLocal,不同key值都可以放在MDC中。

public class LogbackMDCAdapter implements MDCAdapter {

final ThreadLocal> copyOnThreadLocal = new ThreadLocal();

...

}

如果想要串联请求日志,需要设计一个TraceId,通过拦截器或者Filter的方式,设置到MDC中,配置对应的日志格式,让日志打印对应的traceId,这样我们就能通过TraceId完整跟踪一次请求的日志。

public class WebAuthInterceptor extends HandlerInterceptorAdapter {

@Autowired

private WebUserUtil webUserUtil;

@Override

public boolean preHandle(...)

//生成和设置traceId

MDC.put("traceId",traceId);

}

@Override

public void afterCompletion(...)

//清除traceId

MDC.clear();

}

}

切记拦截器的后置逻辑需要清除TraceId

日志格式配置,%X{traceId}来获取MDC中的traceId,注意与put时key的名字保持一致

//此处省略了其他格式配置

3.解决SimpleDateFormat线程安全问题

把SimpleDateFormat设置到ThreadLocal中,每个线程保留自己的副本,实现线程隔离,保证线程安全

public class ThreadLocalDateFormat {

private static final ThreadLocal dateFormatHolder =

ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static SimpleDateFormat getDateFormat() {

return dateFormatHolder.get();

}

}

// 使用方法

SimpleDateFormat dateFormat = ThreadLocalDateFormat.getThreadLocalDateFormat();

String formattedDate = dateFormat.format(new Date());

当然,也可以通过使用DateTimeFormatter,DateTimeFormatter是Java8引入的,它是不可变的且线程安全的。

4.PageHelper存储分页信息

pageHelper的实现也使用到了ThreadLocal,调用startPage是把分页信息存储到LOCAL_PAGE中,在对应SQL上拼接分页信息,执行完SQL的逻辑后,会把分页信息clear掉。

public abstract class PageMethod {

protected static final ThreadLocal LOCAL_PAGE =

new ThreadLocal();

}

PageHelper使用不当极易导致bug,详见我的另一篇文章介绍一次排查PageHelper的坑爹问题,

《坑爹啊,注释无用代码竟会导致bug!又被PageHelper坑了

ThreadLocal最佳实践

1.使用完后一定要显示的进行remove

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。

所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

利用ThreadLocal来实现的一些组件和工具,也要按照这个最佳实践,如PageHelper和MDC等。

2.把ThreadLocal设置为静态常量

把ThreadLocal设置成静态常量,并提供专门对外使用的静态方法set/get/clear,这样能避免重复创建,且使用更加方便。注意,此条恰好构成了导致内存泄漏的条件,必须配和第一条使用。

以上就是木木熊,对于ThreadLocal的简单介绍。部分问题并没有进行深入探究,如果大家有什么疑问和建议,欢迎评论区讨论~~

欢迎大家点赞-评论-关注,另外也可以关注公众号【程序员木木熊】,了解更多后端技术知识!!

微信公众号海量Java、架构、面试、算法资料免费送~

最新发表
友情链接