深入理解ThreadLocal
什么是ThreadLocal?
ThreadLocal
是java.lang
包下的一个线程工具类,它可以保存每个线程的专属变量,实现不同线程之间的数据隔离,从而避免多线程环境下的资源竞争和线程安全问题。
一、ThreadLocal
能做什么?有哪些使用场景?
保存当前请求下的用户信息,在后续调用链路中直接获取,而不是通过方法入参层层传递
创建
UserContext
类,使用ThreadLocal
保存用户信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class UserContext {
private static final ThreadLocal<UserInfo> USER_HOLDER = new ThreadLocal<>();
public static void setCurrentUser(UserInfo userInfo) {
USER_HOLDER.set(userInfo);
}
public static Long getCurrentUserId() {
return USER_HOLDER.get().getId();
}
public static String getCurrentUserOpenId() {
return USER_HOLDER.get().getOpenid();
}
public static void remove() {
USER_HOLDER.remove();
}
}自定义拦截器重写
preHandle()
方法,在请求到达controller前设置用户信息==注意:==在
afterCompletion
方法中进行清理操作,防止ThreadLocal
内存泄漏1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class TokenInterceptor implements HandlerInterceptor {
private JwtUtil jwtUtil;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录接口放行
if (request.getRequestURI().contains("/auth/login")) {
return true;
}
// 验证请求头token
String bearerToken = getTokenFromHeader(request);
if (Strings.isNullOrEmpty(bearerToken) || jwtUtil.verifyToken(bearerToken)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"token无效或已过期\"}");
return false;
}
// 从token中获取用户信息
UserInfo userInfo = jwtUtil.getUserInfo(bearerToken);
// 将用户信息保存到ThreadLocal中
UserContext.setCurrentUser(userInfo);
log.info("设置用户信息成功, userId: {}, openid: {}", UserContext.getCurrentUserId(), UserContext.getCurrentUserOpenId());
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 及时清理ThreadLocal,防止内存泄漏
UserContext.remove();
}
private String getTokenFromHeader(HttpServletRequest request) {
String bearerHeader = request.getHeader("Authorization");
if (!Strings.isNullOrEmpty(bearerHeader) && bearerHeader.startsWith("Bearer ")) {
return bearerHeader.substring(7);
}
return null;
}
}在业务层使用
UserContext
获取当前请求下的用户信息1
2
3
4
5public UserStats getUserStats() {
Long userId = UserContext.getCurrentUserId();
UserInfo userInfo = UserContext.getCurrentUser();
...
}
Spring事务管理
TransactionSynchronizationManager
内部大量使用ThreadLocal
保存事务相关信息AbstractPlatformTransactionManager
、DataSourceTransactionManager
、JdbcTemplate
等类间接使用ThreadLocal
管理事务1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
...
}保存非线程安全的类对象
当然能使用线程安全的类更好,如
DateTimeFormatter
1
2
3
4
5
6
7
8
9
10
11
12
13public class DateUtil {
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return dateFormatThreadLocal.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return dateFormatThreadLocal.get().parse(dateStr);
}
...
}日志跟踪
1
2
3
4
5
6
7
8
9
10
11
12
13public class TraceContext {
private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
public static void set(String traceId) {
traceIdHolder.set(traceId);
}
public static String get() {
return traceIdHolder.get();
}
public static void clear() {
traceIdHolder.remove();
}
}
二、ThreadLocal
实现原理是什么?
Thread
类中的两个成员变量1
2
3
4
5// ThreadLocalMap 才是实际保存线程独立变量副本的地方
ThreadLocal.ThreadLocalMap threadLocals = null;
// 同上,和InheritableThreadLocal 有关,下面会提到
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;ThreadLocalMap
ThreadLocalMap
是ThreadLocal
中的静态内部类,它是真正保存线程独立变量副本的数据结构它有点类似
HashMap
,底层使用数组存储,Key是当前ThreadLocal
对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
}ThreadLocal
可以看到,
ThreadLocal
实际上充当了“管家”角色,它对外提供了ThreadLocalMap
的访问操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}示例
对于保存当前请求对应的用户信息场景,请求1和请求2在自定义拦截器中向各自线程的
ThreadLocalMap
中添加了{USER_HOLDER:userInfo1}、{USER_HOLDER:userInfo2}的数据每个请求后续调用链路中使用
USER_HOLDER.get()
方法获取用户信息时,当前线程先获取到自己的ThreadLocalMap
,然后根据KEY(即USER_HOLDER对象)获取相应的VALUE(即userInfo1、userInfo2对象)1
private static final ThreadLocal<UserInfo> USER_HOLDER = new ThreadLocal<>();
三、InheritableThreadLocal
是什么?有什么作用?
ThreadLocal
的局限性ThreadLocalMap
是线程私有的,在子线程中无法获取父线程中的ThreadLocal
信息,只能通过传参方式获取;而InheritableThreadLocal
就解决了这个问题,子线程可以直接通过get()
方法获取到父线程中的ThreadLocal
信息1
2
3
4
5
6
7
8
9// 主线程设置用户信息
userContext.set("AdminUser");
System.out.println("主线程用户: " + userContext.get()); // 输出: AdminUser
// 创建子线程处理任务
new Thread(() -> {
System.out.println("子线程用户: " + userContext.get()); // 输出: null
// 子线程无法获取父线程的上下文信息
}).start();InheritableThreadLocal
从字面看就是”可继承的ThreadLocal
“,实际上它也确实是ThreadLocal
的子类还记得这里吗?下面的
t.inheritableThreadLocals
就是Thread
类中定义的ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
另外,
InheritableThreadLocal
重写了getMap()
和createMap()
方法,当使用InheritableThreadLocal
时,会将信息保存到Thread
类的inheritableThreadLocals
变量中1
2
3
4
5
6
7
8
9
10
11public class InheritableThreadLocal<T> extends ThreadLocal<T> {
public InheritableThreadLocal() {}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}InheritableThreadLocal
是如何获取到父线程的ThreadLocal
信息的?JDK8及以前,创建线程时,构造方法会调用下面的
init()
方法,该方法中会判断父线程中inheritableThreadLocals
属性是否为null,如果不为null,则将父线程的inheritableThreadLocals
拷贝一份给自己1
2
3
4
5
6
7
8
9
10
11
12
13
14private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}JDK9及以后,相关代码直接放到构造方法中了
1
2
3
4
5
6
7
8
9private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
四、ThreadLocal
产生内存泄露的原因?
ThreadLocalMap
的内部结构ThreadLocalMap
底层是一个Entry数组- Entry的key是
ThreadLocal
类型,它是一个弱引用(==弱引用对象如果没有强引用就会被GC回收==),而value是一个强引用(Object对象)
1
2
3
4
5
6
7
8
9
10
11static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;当Entry的key即
ThreadLocal
对象没有强引用指向它后,下次GC回收后Entry中的key变为null,而对应的value由于是强引用无法被GC回收,因此仍然存在于ThreadLocalMap
中,如果线程一直存活,那么ThreadLocalMap
也会一直存在,因此key为null的Entry无法被GC回收,造成内存泄漏内存泄漏的条件
ThreadLocal
对象被回收(局部变量、匿名对象、手动置空)- 没有显示调用remove()方法清理
- 线程持续存在(如线程池),
ThreadLocalMap
一直存活
五、使用ThreadLocal
时要注意哪些点?
注意线程池使用场景
由于线程池中的线程是复用的,如果线程A使用完
ThreadLocal
后没有清理,后续线程A继续执行任务会遇到“脏数据”使用完及时清理,避免内存泄漏
虽然
ThreadLocal
的get()
、set()
、remove()
方法中有尝试调用expungeStaleEntry()
避免使用
ThreadLocal
存储大对象