ThreadLocal 详解
简介
ThreadLocal 是 Java 提供的一种线程隔离机制,它使得每个线程都可以独立地访问某个变量的副本,从而避免线程安全问题,无需额外的同步操作。本文详细介绍 ThreadLocal 的概念、原理、使用场景以及注意事项。
基本概念
ThreadLocal 是一个泛型类,它为每个使用该变量的线程提供了独立的变量副本,每个线程都可以独立地修改自己的副本,而不会影响其他线程中的副本。这种特性使得 ThreadLocal 成为一种简单的线程安全实现机制。
ThreadLocal 的核心 API
// 创建 ThreadLocal 对象
ThreadLocal<T> threadLocal = new ThreadLocal<>();
// 设置当前线程的 ThreadLocal 变量值
threadLocal.set(value);
// 获取当前线程的 ThreadLocal 变量值
T value = threadLocal.get();
// 移除当前线程的 ThreadLocal 变量
threadLocal.remove();
// 设置初始值(可通过继承 ThreadLocal 并重写此方法实现)
protected T initialValue() {
return null;
}
// Java 8 引入的创建方式,提供初始值
ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> initialValue);
工作原理
内部实现
ThreadLocal 的实现原理依赖于 Thread 类中的一个特殊变量 ThreadLocalMap
:
// Thread 类中的相关字段
ThreadLocal.ThreadLocalMap threadLocals = null;
每个线程都持有一个 ThreadLocalMap
类型的成员变量,该成员变量存储了以 ThreadLocal 对象为 key,线程本地变量为 value 的键值对。
当调用 ThreadLocal 对象的 set(T value) 方法时,实际上是将值存储在当前线程的 ThreadLocalMap
中,而 ThreadLocal 对象本身作为 key。
当调用 ThreadLocal 对象的 get() 方法时,会以 ThreadLocal 对象为 key,从当前线程的 ThreadLocalMap
中获取关联的值。
源码分析
ThreadLocal 的核心方法实现:
// 设置值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 获取值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 移除值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
内存模型示意图
使用场景
ThreadLocal 在很多场景下都非常有用,尤其是那些需要在多个方法中传递上下文信息但又不想通过参数传递的场景。
1. 用户会话管理
在 Web 应用中存储用户会话信息,避免在各个方法间传递用户信息。
public class UserContext {
private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) {
userThreadLocal.set(user);
}
public static User getUser() {
return userThreadLocal.get();
}
public static void clear() {
userThreadLocal.remove();
}
}
// 在请求处理开始时设置
@WebFilter(urlPatterns = "/*")
public class UserContextFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
User user = // 从请求中获取用户信息
UserContext.setUser(user);
chain.doFilter(request, response);
} finally {
UserContext.clear(); // 重要:请求结束时清理
}
}
}
// 在任何地方使用
public void businessMethod() {
User currentUser = UserContext.getUser();
// 使用用户信息处理业务逻辑
}
2. 数据库连接管理
在不使用连接池的情况下,使用 ThreadLocal 存储数据库连接。
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
try {
return DriverManager.getConnection(DB_URL, USER, PASSWORD);
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public static Connection getConnection() {
return connectionHolder.get();
}
public static void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 处理异常
}
}
connectionHolder.remove();
}
}
3. 事务管理
在声明式事务管理中,使用 ThreadLocal 存储当前事务信息。
public class TransactionManager {
private static final ThreadLocal<Transaction> transactionThreadLocal = new ThreadLocal<>();
public static void beginTransaction() {
Transaction transaction = new Transaction();
transaction.begin();
transactionThreadLocal.set(transaction);
}
public static void commitTransaction() {
Transaction transaction = transactionThreadLocal.get();
if (transaction != null) {
transaction.commit();
transactionThreadLocal.remove();
}
}
public static void rollbackTransaction() {
Transaction transaction = transactionThreadLocal.get();
if (transaction != null) {
transaction.rollback();
transactionThreadLocal.remove();
}
}
public static Transaction getCurrentTransaction() {
return transactionThreadLocal.get();
}
}
4. 请求追踪和日志记录
为每个请求分配一个唯一标识符,用于追踪日志。
public class RequestIdContext {
private static final ThreadLocal<String> requestIdThreadLocal = ThreadLocal.withInitial(() ->
UUID.randomUUID().toString());
public static String getRequestId() {
return requestIdThreadLocal.get();
}
public static void setRequestId(String requestId) {
requestIdThreadLocal.set(requestId);
}
public static void clear() {
requestIdThreadLocal.remove();
}
}
// 在日志中使用
public class RequestLogger {
private static final Logger logger = LoggerFactory.getLogger(RequestLogger.class);
public static void logInfo(String message) {
logger.info("[RequestId: {}] {}", RequestIdContext.getRequestId(), message);
}
}
5. 简化参数传递
在复杂的调用链中简化参数传递,避免方法签名臃肿。
public class ContextHolder {
private static final ThreadLocal<Map<String, Object>> contextThreadLocal =
ThreadLocal.withInitial(HashMap::new);
public static void set(String key, Object value) {
contextThreadLocal.get().put(key, value);
}
public static Object get(String key) {
return contextThreadLocal.get().get(key);
}
public static void clear() {
contextThreadLocal.get().clear();
contextThreadLocal.remove();
}
}
最佳实践
正确使用 ThreadLocal 的注意事项
- 始终在 finally 块中调用 remove() 方法
ThreadLocal 对象使用完毕后,需要调用 remove() 方法清除当前线程的值,尤其是在线程池环境中,避免内存泄漏和数据混淆。
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(value);
// 使用 threadLocal
} finally {
threadLocal.remove(); // 清理资源
}
- 优先使用 initialValue() 或 withInitial() 设置初始值
通过重写 initialValue() 方法或使用 withInitial() 静态方法设置初始值,避免重复检查和设置逻辑。
// 方法 1:继承 ThreadLocal 并重写 initialValue()
public class MyThreadLocal extends ThreadLocal<Date> {
@Override
protected Date initialValue() {
return new Date();
}
}
// 方法 2:使用 withInitial() 静态方法
ThreadLocal<Date> dateThreadLocal = ThreadLocal.withInitial(Date::new);
- 避免直接操作 ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的内部实现细节,不应该直接操作它。
- 注意子线程的继承问题
ThreadLocal 变量在创建子线程时不会自动继承。如果需要在子线程中访问父线程的 ThreadLocal 变量,可以使用 InheritableThreadLocal。
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("parent value");
Thread childThread = new Thread(() -> {
System.out.println("Child thread value: " + inheritableThreadLocal.get());
});
childThread.start();
- 注意在异步任务中的使用
在使用 CompletableFuture、并行流等异步任务时,ThreadLocal 变量不会自动传递给执行任务的线程。需要手动传递或使用第三方库。
内存泄漏
什么是内存泄漏?
内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间,导致这部分内存无法被再次使用的情况。在 Java 中,内存泄漏具体表现为对象已经不再被程序使用,但由于仍然被某些引用所持有,导致垃圾收集器无法回收它们。
Java 虽然有自动垃圾回收机制,但仍可能发生内存泄漏,主要有以下几种情况:
长生命周期对象持有短生命周期对象的引用:例如,静态集合中保存了大量动态创建的对象,而这些对象不再使用但又无法被回收。
未关闭的资源:如文件流、数据库连接、网络连接等资源未正确关闭。
不正确的 equals 和 hashCode 实现:导致对象无法从集合中移除。
内部类和匿名内部类的隐式引用:内部类持有外部类的引用,可能导致外部类无法被回收。
回调和监听器:注册了回调或监听器,但使用完毕后未及时移除。
内存泄漏的危害
可用内存减少,导致应用程序性能下降
严重时可能引发 OutOfMemoryError 错误,导致应用崩溃
对于长时间运行的应用(如服务器应用),即使是微小的内存泄漏也会随着时间累积,最终导致严重问题
ThreadLocal 的内存泄漏问题
ThreadLocal 可能导致内存泄漏,主要原因是:
ThreadLocalMap 的 Entry 使用 ThreadLocal 的弱引用作为 key,但对 value 是强引用。
如果 ThreadLocal 对象被垃圾回收,Entry 中的 key 变为 null,但 value 仍然存在且无法被访问。
如果线程长时间存活(如线程池中的线程),这些无法访问的 value 会一直占用内存。
具体来说,ThreadLocal 内存泄漏的过程如下:
场景1:程序不再使用某 ThreadLocal 变量,该变量变为垃圾
场景2:由于 ThreadLocalMap 中的 Entry 对 key 使用的是弱引用,这些 key 会被垃圾回收
场景3:Entry 中的 value 仍然被 ThreadLocalMap 强引用,但由于 key 为 null,无法再通过常规方式访问到这些 value
场景4:如果线程一直存活(尤其是线程池的核心线程),这些无法访问的 value 会一直占用内存空间
正确的使用方式是在不需要时调用 remove() 方法清除值:
public class SafeThreadLocalUsage implements AutoCloseable {
private final ThreadLocal<Resource> resourceThreadLocal = new ThreadLocal<>();
public void useResource() {
Resource resource = new Resource();
resourceThreadLocal.set(resource);
try {
// 使用资源
} finally {
resourceThreadLocal.remove(); // 防止内存泄漏
}
}
@Override
public void close() {
resourceThreadLocal.remove(); // 确保清理
}
}
高级特性
InheritableThreadLocal
InheritableThreadLocal 是 ThreadLocal 的子类,它允许子线程访问父线程中设置的值。当创建子线程时,子线程会复制父线程的 InheritableThreadLocal 变量。
public class InheritableThreadLocalExample {
private static final InheritableThreadLocal<String> inheritableThreadLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("父线程值");
Thread childThread = new Thread(() -> {
System.out.println("子线程访问值: " + inheritableThreadLocal.get());
// 修改子线程的值不影响父线程
inheritableThreadLocal.set("子线程修改的值");
System.out.println("子线程修改后: " + inheritableThreadLocal.get());
Thread grandChildThread = new Thread(() -> {
// 孙子线程继承子线程的值
System.out.println("孙子线程访问值: " + inheritableThreadLocal.get());
});
grandChildThread.start();
});
childThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 父线程的值不受子线程修改的影响
System.out.println("父线程值保持不变: " + inheritableThreadLocal.get());
}
}
注意事项:
InheritableThreadLocal 只会在子线程创建时传递一次值,之后父子线程的值相互独立。
在线程池环境中,线程可能被重用,InheritableThreadLocal 的值可能不符合预期。
TransmittableThreadLocal
TransmittableThreadLocal 是阿里开源的增强 ThreadLocal,它解决了 InheritableThreadLocal 在线程池下的局限,使变量可以正确传递给线程池中的任务。
// 添加依赖
// <dependency>
// <groupId>com.alibaba</groupId>
// <artifactId>transmittable-thread-local</artifactId>
// <version>2.14.2</version>
// </dependency>
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TransmittableThreadLocalExample {
private static final TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
context.set("主线程设置的值");
// 使用 TtlRunnable 包装原始任务
Runnable task = TtlRunnable.get(() -> {
System.out.println("线程池中的线程获取值: " + context.get());
});
executorService.execute(task);
// 修改主线程的值
context.set("主线程修改后的值");
executorService.execute(task);
executorService.shutdown();
}
}
常见问题
ThreadLocal 的实现原理是什么?
ThreadLocal 的实现原理是每个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表存储以 ThreadLocal 为 key,实际变量副本为 value 的键值对。当通过 ThreadLocal 变量存取值时,实际上是在当前线程的 ThreadLocalMap 中进行操作。
ThreadLocal 如何解决哈希冲突?
ThreadLocalMap 采用开放地址法解决哈希冲突。当出现冲突时,会往后找一个空位置存放。ThreadLocalMap 初始大小为 16,负载因子为 2/3,当表中的 Entry 数量达到阈值时进行扩容。
ThreadLocal 内存泄漏的原因及如何避免?
原因:
ThreadLocalMap 的 Entry 使用 ThreadLocal 的弱引用作为 key,但对 value 是强引用。
当 ThreadLocal 对象不再被引用时,Entry 中的 key 变为 null,但 value 仍然存在。
如果线程长时间存活,这些 value 会一直占用内存。
避免方法:
使用完 ThreadLocal 后显式调用 remove() 方法清除。
将 ThreadLocal 变量定义为静态变量,延长其生命周期。
使用 try-finally 确保清理资源。
ThreadLocal 和 synchronized 有什么区别?
特性 | ThreadLocal | synchronized |
---|---|---|
实现原理 | 每个线程独立副本 | 同步访问共享资源 |
侧重点 | 线程隔离 | 资源同步 |
性能影响 | 较小 | 可能造成阻塞 |
适用场景 | 线程独立数据 | 多线程共享数据 |
内存影响 | 每个线程一份副本 | 一份数据多线程共享 |
为什么 ThreadLocalMap 的 key 使用弱引用?
避免内存泄漏:如果 key 使用强引用,即使外部不再引用 ThreadLocal 对象,ThreadLocalMap 仍会保持对它的引用,导致无法被垃圾回收。
使用弱引用后,当外部不再引用 ThreadLocal 对象时,它可以被垃圾回收,避免长期持有无用的 ThreadLocal 对象。
但同时也导致了值的泄漏问题,因为虽然 key 被回收,value 仍然存在。