
在 Java 并发编程的世界里,ThreadLocal 是一个看似简单却威力无穷的工具。它就像一位隐形的助手,默默地为每个线程维护着一份专属的数据副本,让我们在多线程环境下无需为共享变量的同步问题头疼。
然而,很多开发者对 ThreadLocal 的理解仅仅停留在 "线程本地变量" 这个表层概念,并没有真正发挥它的潜力,甚至在使用中频繁踩坑。你是否也曾遇到过这样的困惑:
本文将带你全面深入 ThreadLocal 的世界,从底层原理到实战应用,详细解析 6 大常见应用场景,并通过可运行的代码示例展示最佳实践。无论你是刚接触并发编程的新手,还是希望提升并发处理能力的资深开发者,都能从本文中获得实用的知识和技巧。
在探讨应用场景之前,我们首先需要深入理解 ThreadLocal 的工作原理。只有知其然且知其所以然,才能在实际开发中灵活运用并避免常见陷阱。
ThreadLocal,即线程本地变量,是 Java 提供的一种线程封闭机制。它允许我们创建一个变量,该变量在每个线程中都有一个独立的副本,线程对变量的操作只会影响到自己的副本,不会影响其他线程。

这种机制与传统的共享变量同步方式截然不同:

在 JDK 17 中,ThreadLocal 的实现机制如下:
每个 Thread 对象内部都维护着一个 ThreadLocalMap 对象,这个 Map 的 key 是 ThreadLocal 实例本身,value 是线程的变量副本。
// Thread类中的相关代码
public class Thread implements Runnable {
// 每个线程都有一个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
// ThreadLocal类中的内部类
static class ThreadLocalMap {
// 存储数据的Entry数组,采用线性探测法解决哈希冲突
private Entry[] table;
// Entry继承自WeakReference,key是弱引用的ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal 的工作流程:

这种实现方式的优点:
ThreadLocal 类提供了几个核心方法:
public class ThreadLocal<T> {
/**
* 初始化ThreadLocal的值,默认返回null
* 子类可以重写此方法提供初始值
*/
protected T initialValue() {
return null;
}
/**
* 获取当前线程的变量副本
* 如果不存在,则通过initialValue()初始化
*/
public T get() {
// 实现细节
}
/**
* 设置当前线程的变量副本
*/
public void set(T value) {
// 实现细节
}
/**
* 移除当前线程的变量副本
* JDK 1.5新增,用于避免内存泄漏
*/
public void remove() {
// 实现细节
}
/**
* 创建一个ThreadLocal,其初始值由Supplier提供
* JDK 8新增
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
}
在多层调用的场景中, ThreadLocal 可以用来传递上下文信息,避免通过方法参数层层传递的麻烦。最典型的例子就是 Web 应用中的用户登录信息传递。
在 Web 开发中,用户登录后,其身份信息需要在整个请求处理过程中被访问到,从 Controller 到 Service 再到 Dao 层。使用 ThreadLocal 可以优雅地实现这一点。
首先,创建一个用户上下文工具类:
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
/**
* 用户上下文工具类,用于在当前线程中存储和获取用户信息
*
* @author ken
*/
@Slf4j
public class UserContextHolder {
/**
* ThreadLocal实例,存储用户上下文
*/
private static final ThreadLocal<UserContext> USER_CONTEXT = ThreadLocal.withInitial(UserContext::new);
/**
* 设置当前线程的用户上下文
*
* @param userContext 用户上下文对象
*/
public static void setUserContext(UserContext userContext) {
if (ObjectUtils.isEmpty(userContext)) {
log.error("设置的用户上下文不能为空");
throw new IllegalArgumentException("用户上下文不能为空");
}
USER_CONTEXT.set(userContext);
log.debug("已设置用户上下文: {}", userContext.getUserId());
}
/**
* 获取当前线程的用户上下文
*
* @return 当前线程的用户上下文,如果不存在则返回初始空对象
*/
public static UserContext getUserContext() {
UserContext context = USER_CONTEXT.get();
log.debug("获取用户上下文: {}", context.getUserId());
return context;
}
/**
* 清除当前线程的用户上下文
* 必须在请求处理完成后调用,避免内存泄漏
*/
public static void clear() {
UserContext context = USER_CONTEXT.get();
log.debug("清除用户上下文: {}", context.getUserId());
USER_CONTEXT.remove();
}
/**
* 用户上下文类,存储用户相关信息
*/
@Getter
@Setter
public static class UserContext {
private String userId;
private String username;
private String nickname;
private String role;
// 可以根据需要添加更多字段
}
}
然后,在拦截器中设置和清除用户上下文:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 用户上下文拦截器,用于在请求处理前后管理用户上下文
*
* @author ken
*/
@Slf4j
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("开始处理请求,设置用户上下文");
// 实际应用中,这里应该从请求头或会话中获取真实的用户信息
UserContextHolder.UserContext context = new UserContextHolder.UserContext();
context.setUserId("user_" + System.currentTimeMillis() % 1000);
context.setUsername("test_user");
context.setNickname("测试用户");
context.setRole("USER");
UserContextHolder.setUserContext(context);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("请求处理完成,清除用户上下文");
// 必须清除,否则可能导致内存泄漏
UserContextHolder.clear();
}
}
配置拦截器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC配置类
*
* @author ken
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册用户上下文拦截器,对所有请求生效
registry.addInterceptor(new UserContextInterceptor()).addPathPatterns("/**");
}
}
在 Controller、Service、Dao 等各层中使用:
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试控制器,演示用户上下文的使用
*
* @author ken
*/
@RestController
@RequestMapping("/test")
@Slf4j
@Tag(name = "测试接口", description = "用于演示ThreadLocal用户上下文的使用")
public class TestController {
private final TestService testService;
public TestController(TestService testService) {
this.testService = testService;
}
@GetMapping("/context")
@Operation(summary = "测试用户上下文", description = "演示在控制器中获取用户上下文")
public String testContext() {
// 从ThreadLocal中获取用户上下文
UserContextHolder.UserContext context = UserContextHolder.getUserContext();
log.info("Controller层获取到用户信息: {}", context.getUsername());
// 调用Service层
testService.process();
return "success: " + context.getUsername();
}
}
/**
* 测试服务类
*
* @author ken
*/
@Slf4j
@org.springframework.stereotype.Service
public class TestService {
private final TestDao testDao;
public TestService(TestDao testDao) {
this.testDao = testDao;
}
public void process() {
// 在Service层获取用户上下文
UserContextHolder.UserContext context = UserContextHolder.getUserContext();
log.info("Service层获取到用户信息: {}", context.getNickname());
// 调用Dao层
testDao.query();
}
}
/**
* 测试数据访问类
*
* @author ken
*/
@Slf4j
@org.springframework.stereotype.Repository
public class TestDao {
public void query() {
// 在Dao层获取用户上下文
UserContextHolder.UserContext context = UserContextHolder.getUserContext();
log.info("Dao层获取到用户信息: {}", context.getUserId());
// 实际应用中这里会执行数据库操作,可能使用用户ID作为查询条件或记录操作人
}
}
这种方式的优势:
在分布式系统中,为了追踪一个请求的完整链路,通常会使用一个唯一的链路 ID(Trace ID)。ThreadLocal 可以用来在当前服务的各个组件中传递这个 Trace ID。
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.util.UUID;
/**
* 分布式追踪上下文工具类
*
* @author ken
*/
@Slf4j
public class TraceContextHolder {
/**
* ThreadLocal实例,存储追踪上下文
*/
private static final ThreadLocal<TraceContext> TRACE_CONTEXT = ThreadLocal.withInitial(TraceContext::new);
/**
* 初始化追踪上下文,如果不存在则生成新的Trace ID
*/
public static void init() {
TraceContext context = TRACE_CONTEXT.get();
if (ObjectUtils.isEmpty(context.getTraceId())) {
context.setTraceId(generateTraceId());
context.setSpanId(generateSpanId());
TRACE_CONTEXT.set(context);
log.debug("初始化追踪上下文, traceId: {}", context.getTraceId());
} else {
log.debug("追踪上下文已存在, traceId: {}", context.getTraceId());
}
}
/**
* 设置追踪上下文
*
* @param traceContext 追踪上下文对象
*/
public static void setTraceContext(TraceContext traceContext) {
if (ObjectUtils.isEmpty(traceContext)) {
log.error("设置的追踪上下文不能为空");
throw new IllegalArgumentException("追踪上下文不能为空");
}
TRACE_CONTEXT.set(traceContext);
log.debug("已设置追踪上下文, traceId: {}", traceContext.getTraceId());
}
/**
* 获取当前线程的追踪上下文
*
* @return 当前线程的追踪上下文
*/
public static TraceContext getTraceContext() {
TraceContext context = TRACE_CONTEXT.get();
// 如果还未初始化,自动初始化
if (ObjectUtils.isEmpty(context.getTraceId())) {
init();
}
return context;
}
/**
* 清除当前线程的追踪上下文
*/
public static void clear() {
TraceContext context = TRACE_CONTEXT.get();
log.debug("清除追踪上下文, traceId: {}", context.getTraceId());
TRACE_CONTEXT.remove();
}
/**
* 生成新的Trace ID
*
* @return 新的Trace ID
*/
private static String generateTraceId() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
/**
* 生成新的Span ID
*
* @return 新的Span ID
*/
private static String generateSpanId() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
}
/**
* 追踪上下文类
*/
@Getter
@Setter
public static class TraceContext {
private String traceId; // 整个调用链路的唯一标识
private String spanId; // 当前服务的调用标识
private String parentSpanId; // 父服务的调用标识
}
}
在拦截器中初始化和传递 Trace ID:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 分布式追踪拦截器
*
* @author ken
*/
@Slf4j
public class TraceInterceptor implements HandlerInterceptor {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String SPAN_ID_HEADER = "X-Span-Id";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("开始处理请求,初始化追踪上下文");
// 从请求头获取Trace ID,如果没有则生成新的
String traceId = request.getHeader(TRACE_ID_HEADER);
String spanId = request.getHeader(SPAN_ID_HEADER);
TraceContextHolder.TraceContext context = new TraceContextHolder.TraceContext();
if (org.springframework.util.StringUtils.hasText(traceId)) {
// 如果有上级服务传递的Trace ID,直接使用
context.setTraceId(traceId);
context.setParentSpanId(spanId);
// 生成当前服务的Span ID
context.setSpanId(TraceContextHolder.generateSpanId());
log.debug("接收到上级服务的追踪信息, traceId: {}", traceId);
} else {
// 没有则初始化新的追踪上下文
TraceContextHolder.init();
context = TraceContextHolder.getTraceContext();
log.debug("生成新的追踪信息, traceId: {}", context.getTraceId());
}
TraceContextHolder.setTraceContext(context);
// 将Trace ID设置到响应头,方便后续服务获取
response.setHeader(TRACE_ID_HEADER, context.getTraceId());
response.setHeader(SPAN_ID_HEADER, context.getSpanId());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("请求处理完成,清除追踪上下文");
TraceContextHolder.clear();
}
}
通过这种方式,我们可以在日志中包含 Trace ID,从而轻松追踪一个请求在分布式系统中的完整路径。
在多线程环境下,某些工具类可能因为存在状态而导致线程安全问题。使用 ThreadLocal 可以为每个线程提供独立的工具类实例,从而避免同步和线程安全问题。
SimpleDateFormat 是 Java 中常用的日期格式化类,但它是非线程安全的。在多线程环境下共享一个 SimpleDateFormat 实例会导致各种奇怪的问题。
使用 ThreadLocal 为每个线程提供独立的 SimpleDateFormat 实例:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* 线程安全的日期格式化工具类
*
* @author ken
*/
@Slf4j
public class ThreadSafeDateUtils {
/**
* 默认日期格式:yyyy-MM-dd HH:mm:ss
*/
public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 存储不同格式的SimpleDateFormat实例
* key: 日期格式字符串
* value: 对应的SimpleDateFormat实例
*/
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTERS = ThreadLocal.withInitial(
() -> new SimpleDateFormat(DEFAULT_PATTERN)
);
/**
* 将日期格式化为字符串
*
* @param date 日期对象,不能为null
* @param pattern 日期格式,如果为null则使用默认格式
* @return 格式化后的日期字符串
* @throws IllegalArgumentException 如果date为null
*/
public static String format(Date date, String pattern) {
if (Objects.isNull(date)) {
log.error("日期对象不能为空");
throw new IllegalArgumentException("日期对象不能为空");
}
String actualPattern = StringUtils.hasText(pattern) ? pattern : DEFAULT_PATTERN;
SimpleDateFormat sdf = getSimpleDateFormat(actualPattern);
return sdf.format(date);
}
/**
* 将日期格式化为字符串,使用默认格式
*
* @param date 日期对象,不能为null
* @return 格式化后的日期字符串
* @throws IllegalArgumentException 如果date为null
*/
public static String format(Date date) {
return format(date, DEFAULT_PATTERN);
}
/**
* 将字符串解析为日期
*
* @param dateStr 日期字符串,不能为null或空
* @param pattern 日期格式,如果为null则使用默认格式
* @return 解析后的日期对象
* @throws IllegalArgumentException 如果dateStr为null或空
* @throws ParseException 如果解析失败
*/
public static Date parse(String dateStr, String pattern) throws ParseException {
if (!StringUtils.hasText(dateStr)) {
log.error("日期字符串不能为空");
throw new IllegalArgumentException("日期字符串不能为空");
}
String actualPattern = StringUtils.hasText(pattern) ? pattern : DEFAULT_PATTERN;
SimpleDateFormat sdf = getSimpleDateFormat(actualPattern);
return sdf.parse(dateStr);
}
/**
* 将字符串解析为日期,使用默认格式
*
* @param dateStr 日期字符串,不能为null或空
* @return 解析后的日期对象
* @throws IllegalArgumentException 如果dateStr为null或空
* @throws ParseException 如果解析失败
*/
public static Date parse(String dateStr) throws ParseException {
return parse(dateStr, DEFAULT_PATTERN);
}
/**
* 获取当前线程的SimpleDateFormat实例,如果格式不同则更新格式
*
* @param pattern 日期格式
* @return 当前线程的SimpleDateFormat实例
*/
private static SimpleDateFormat getSimpleDateFormat(String pattern) {
SimpleDateFormat sdf = DATE_FORMATTERS.get();
// 如果当前格式与需要的格式不同,则更新格式
if (!sdf.toPattern().equals(pattern)) {
sdf.applyPattern(pattern);
}
return sdf;
}
/**
* 清除当前线程的SimpleDateFormat实例
* 主要用于测试或特殊场景
*/
public static void clear() {
DATE_FORMATTERS.remove();
log.debug("已清除当前线程的SimpleDateFormat实例");
}
}
测试线程安全性:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.text.ParseException;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 线程安全日期工具类测试
*
* @author ken
*/
@Slf4j
public class ThreadSafeDateUtilsTest {
private static final int THREAD_COUNT = 10;
private static final int TASK_COUNT = 1000;
@Test
public void testThreadSafety() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
// 测试格式化
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
try {
for (int j = 0; j < TASK_COUNT; j++) {
Date date = new Date(j * 1000L);
String formatted = ThreadSafeDateUtils.format(date);
try {
Date parsed = ThreadSafeDateUtils.parse(formatted);
if (parsed.getTime() != date.getTime()) {
log.error("日期处理错误: 原始时间={}, 格式化后={}, 解析后时间={}",
date.getTime(), formatted, parsed.getTime());
}
} catch (ParseException e) {
log.error("日期解析错误: " + formatted, e);
}
}
} finally {
latch.countDown();
}
});
}
latch.await(1, TimeUnit.MINUTES);
executor.shutdown();
log.info("所有线程执行完毕");
}
}
这种方式相比每次创建新的 SimpleDateFormat 实例性能更好,因为避免了频繁创建和销毁对象的开销;同时又比共享一个实例更安全,不需要同步操作。
在 JDBC 中,数据库连接(Connection)是线程不安全的,通常每个线程应该使用自己的连接。ThreadLocal 可以用来管理连接的生命周期。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* 数据库连接管理工具类,使用ThreadLocal管理连接
*
* @author ken
*/
@Slf4j
public class ConnectionManager {
/**
* 数据库连接URL
*/
private static final String DB_URL = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC";
/**
* 数据库用户名
*/
private static final String DB_USER = "root";
/**
* 数据库密码
*/
private static final String DB_PASSWORD = "password";
/**
* 存储当前线程的数据库连接
*/
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();
static {
try {
// 加载MySQL驱动
Class.forName("com.mysql.cj.jdbc.Driver");
log.info("MySQL驱动加载成功");
} catch (ClassNotFoundException e) {
log.error("加载MySQL驱动失败", e);
throw new RuntimeException("加载MySQL驱动失败", e);
}
}
/**
* 获取当前线程的数据库连接
* 如果当前线程没有连接,则创建一个新的连接并绑定到当前线程
*
* @return 当前线程的数据库连接
* @throws SQLException 如果获取连接失败
*/
public static Connection getConnection() throws SQLException {
Connection connection = CONNECTION_HOLDER.get();
if (ObjectUtils.isEmpty(connection) || connection.isClosed()) {
// 创建新连接
Properties props = new Properties();
props.setProperty("user", DB_USER);
props.setProperty("password", DB_PASSWORD);
connection = DriverManager.getConnection(DB_URL, props);
CONNECTION_HOLDER.set(connection);
log.debug("创建新的数据库连接并绑定到当前线程: {}", connection);
} else {
log.debug("获取当前线程已有的数据库连接: {}", connection);
}
return connection;
}
/**
* 关闭当前线程的数据库连接
* 并从ThreadLocal中移除
*/
public static void closeConnection() {
Connection connection = CONNECTION_HOLDER.get();
if (!ObjectUtils.isEmpty(connection)) {
try {
if (!connection.isClosed()) {
connection.close();
log.debug("关闭数据库连接: {}", connection);
}
} catch (SQLException e) {
log.error("关闭数据库连接失败", e);
} finally {
// 无论是否关闭成功,都从ThreadLocal中移除
CONNECTION_HOLDER.remove();
log.debug("从当前线程移除数据库连接");
}
}
}
/**
* 开启事务
*
* @throws SQLException 如果开启事务失败
*/
public static void beginTransaction() throws SQLException {
Connection connection = getConnection();
connection.setAutoCommit(false);
log.debug("开启事务: {}", connection);
}
/**
* 提交事务
*
* @throws SQLException 如果提交事务失败
*/
public static void commitTransaction() throws SQLException {
Connection connection = getConnection();
connection.commit();
log.debug("提交事务: {}", connection);
}
/**
* 回滚事务
*
* @throws SQLException 如果回滚事务失败
*/
public static void rollbackTransaction() throws SQLException {
Connection connection = getConnection();
connection.rollback();
log.debug("回滚事务: {}", connection);
}
}
使用示例:
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* 订单服务类,演示数据库连接管理
*
* @author ken
*/
@Slf4j
public class OrderService {
/**
* 创建订单
*
* @param orderId 订单ID
* @param userId 用户ID
* @param amount 订单金额
* @throws SQLException 如果数据库操作失败
*/
public void createOrder(String orderId, String userId, double amount) throws SQLException {
try {
// 开启事务
ConnectionManager.beginTransaction();
Connection connection = ConnectionManager.getConnection();
// 插入订单记录
String sql = "INSERT INTO orders (id, user_id, amount, status) VALUES (?, ?, ?, 'PENDING')";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, orderId);
stmt.setString(2, userId);
stmt.setDouble(3, amount);
stmt.executeUpdate();
log.info("订单创建成功: {}", orderId);
}
// 扣减库存等其他操作...
// 提交事务
ConnectionManager.commitTransaction();
log.info("订单事务提交成功: {}", orderId);
} catch (SQLException e) {
// 回滚事务
ConnectionManager.rollbackTransaction();
log.error("订单创建失败,已回滚事务: {}", orderId, e);
throw e;
} finally {
// 关闭连接
ConnectionManager.closeConnection();
}
}
}
这种方式确保了在同一事务中的所有数据库操作使用的是同一个连接,并且每个线程都有自己独立的连接,避免了多线程共享连接导致的问题。
在事务管理中,ThreadLocal 的作用至关重要。它确保了在一个事务中,所有的数据库操作都使用同一个连接,并且这个连接不会被其他线程干扰。
虽然实际开发中我们通常使用 Spring 的声明式事务管理,但了解其底层实现原理有助于我们更好地理解事务管理的本质。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 自定义事务管理器
*
* @author ken
*/
@Slf4j
public class TransactionManager {
/**
* 事务隔离级别:读未提交
*/
public static final int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
/**
* 事务隔离级别:读已提交
*/
public static final int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
/**
* 事务隔离级别:可重复读
*/
public static final int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
/**
* 事务隔离级别:串行化
*/
public static final int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
/**
* 存储当前线程的事务状态
*/
private static final ThreadLocal<TransactionStatus> TRANSACTION_STATUS_HOLDER = new ThreadLocal<>();
/**
* 开启事务
*
* @param isolationLevel 事务隔离级别,如ISOLATION_READ_COMMITTED
* @throws SQLException 如果开启事务失败
*/
public void beginTransaction(int isolationLevel) throws SQLException {
// 检查当前线程是否已经存在事务
TransactionStatus status = TRANSACTION_STATUS_HOLDER.get();
if (!ObjectUtils.isEmpty(status) && status.isActive()) {
log.warn("当前线程已经存在活跃事务,嵌套事务不受支持");
throw new IllegalStateException("当前线程已经存在活跃事务");
}
// 获取连接并设置事务属性
Connection connection = ConnectionManager.getConnection();
connection.setAutoCommit(false);
connection.setTransactionIsolation(isolationLevel);
// 记录事务状态
status = new TransactionStatus();
status.setActive(true);
status.setConnection(connection);
TRANSACTION_STATUS_HOLDER.set(status);
log.debug("开启事务,隔离级别: {}", isolationLevel);
}
/**
* 开启事务,使用默认隔离级别(可重复读)
*
* @throws SQLException 如果开启事务失败
*/
public void beginTransaction() throws SQLException {
beginTransaction(ISOLATION_REPEATABLE_READ);
}
/**
* 提交事务
*
* @throws SQLException 如果提交事务失败
*/
public void commit() throws SQLException {
TransactionStatus status = TRANSACTION_STATUS_HOLDER.get();
if (ObjectUtils.isEmpty(status) || !status.isActive()) {
log.error("没有活跃事务可以提交");
throw new IllegalStateException("没有活跃事务可以提交");
}
try {
status.getConnection().commit();
log.debug("事务提交成功");
} finally {
// 标记事务为已完成
status.setActive(false);
TRANSACTION_STATUS_HOLDER.set(status);
// 关闭连接
ConnectionManager.closeConnection();
}
}
/**
* 回滚事务
*
* @throws SQLException 如果回滚事务失败
*/
public void rollback() throws SQLException {
TransactionStatus status = TRANSACTION_STATUS_HOLDER.get();
if (ObjectUtils.isEmpty(status) || !status.isActive()) {
log.error("没有活跃事务可以回滚");
throw new IllegalStateException("没有活跃事务可以回滚");
}
try {
status.getConnection().rollback();
log.debug("事务回滚成功");
} finally {
// 标记事务为已完成
status.setActive(false);
TRANSACTION_STATUS_HOLDER.set(status);
// 关闭连接
ConnectionManager.closeConnection();
}
}
/**
* 检查当前线程是否有活跃事务
*
* @return 如果有活跃事务则返回true,否则返回false
*/
public boolean hasActiveTransaction() {
TransactionStatus status = TRANSACTION_STATUS_HOLDER.get();
return !ObjectUtils.isEmpty(status) && status.isActive();
}
/**
* 事务状态类
*/
private static class TransactionStatus {
private boolean active;
private Connection connection;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public Connection getConnection() {
return connection;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
}
}
使用示例:
import lombok.extern.slf4j.Slf4j;
import java.sql.SQLException;
/**
* 订单服务类,使用自定义事务管理器
*
* @author ken
*/
@Slf4j
public class OrderServiceWithTxManager {
private final TransactionManager transactionManager;
public OrderServiceWithTxManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* 创建订单并扣减库存
*
* @param orderId 订单ID
* @param userId 用户ID
* @param productId 产品ID
* @param quantity 数量
* @param amount 金额
* @throws SQLException 如果数据库操作失败
*/
public void createOrderAndDeductInventory(String orderId, String userId, String productId, int quantity, double amount) throws SQLException {
try {
// 开启事务
transactionManager.beginTransaction(TransactionManager.ISOLATION_READ_COMMITTED);
// 创建订单
createOrder(orderId, userId, amount);
// 扣减库存
deductInventory(productId, quantity);
// 提交事务
transactionManager.commit();
log.info("订单创建和库存扣减成功,事务已提交: {}", orderId);
} catch (SQLException e) {
// 回滚事务
transactionManager.rollback();
log.error("操作失败,事务已回滚: {}", orderId, e);
throw e;
}
}
/**
* 创建订单
*/
private void createOrder(String orderId, String userId, double amount) throws SQLException {
// 实现见前面的示例
log.info("创建订单: {}", orderId);
}
/**
* 扣减库存
*/
private void deductInventory(String productId, int quantity) throws SQLException {
// 实际实现中会执行UPDATE语句扣减库存
log.info("扣减库存,产品ID: {}, 数量: {}", productId, quantity);
}
}
Spring 的事务管理也是基于类似的原理,通过 ThreadLocal 存储当前事务的连接,并在整个事务过程中复用该连接,确保了事务的 ACID 特性。
在复杂的分页查询中,分页参数(页码、每页条数等)需要在多个方法之间传递。使用 ThreadLocal 可以简化参数传递,尤其是在无法修改方法签名的情况下。
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
/**
* 分页查询上下文工具类
*
* @author ken
*/
@Slf4j
public class PageContextHolder {
/**
* ThreadLocal实例,存储分页参数
*/
private static final ThreadLocal<PageParam> PAGE_PARAM_HOLDER = new ThreadLocal<>();
/**
* 设置分页参数
*
* @param pageNum 页码,从1开始
* @param pageSize 每页条数
*/
public static void setPageParam(int pageNum, int pageSize) {
if (pageNum < 1) {
log.warn("页码不能小于1,自动调整为1");
pageNum = 1;
}
if (pageSize < 1) {
log.warn("每页条数不能小于1,自动调整为10");
pageSize = 10;
}
PageParam param = new PageParam();
param.setPageNum(pageNum);
param.setPageSize(pageSize);
PAGE_PARAM_HOLDER.set(param);
log.debug("设置分页参数: 页码={}, 每页条数={}", pageNum, pageSize);
}
/**
* 获取当前分页参数
* 如果没有设置,返回默认值(第1页,每页10条)
*
* @return 分页参数
*/
public static PageParam getPageParam() {
PageParam param = PAGE_PARAM_HOLDER.get();
if (ObjectUtils.isEmpty(param)) {
log.debug("未设置分页参数,使用默认值: 页码=1, 每页条数=10");
param = new PageParam();
param.setPageNum(1);
param.setPageSize(10);
}
return param;
}
/**
* 清除当前分页参数
*/
public static void clear() {
PageParam param = PAGE_PARAM_HOLDER.get();
if (!ObjectUtils.isEmpty(param)) {
log.debug("清除分页参数: 页码={}, 每页条数={}", param.getPageNum(), param.getPageSize());
} else {
log.debug("清除分页参数: 没有设置分页参数");
}
PAGE_PARAM_HOLDER.remove();
}
/**
* 分页参数类
*/
@Getter
@Setter
public static class PageParam {
private int pageNum; // 页码,从1开始
private int pageSize; // 每页条数
/**
* 计算起始位置(用于SQL查询)
*
* @return 起始位置,从0开始
*/
public int getStart() {
return (pageNum - 1) * pageSize;
}
}
}
在 MyBatis-Plus 中使用:
首先,创建分页拦截器:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
/**
* 基于ThreadLocal的分页拦截器
*
* @author ken
*/
@Slf4j
public class ThreadLocalPaginationInterceptor implements InnerInterceptor {
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 从ThreadLocal获取分页参数
PageContextHolder.PageParam pageParam = PageContextHolder.getPageParam();
// 设置分页参数到RowBounds
rowBounds = new RowBounds(pageParam.getStart(), pageParam.getPageSize());
log.debug("应用分页参数: 起始位置={}, 每页条数={}", pageParam.getStart(), pageParam.getPageSize());
return true;
}
}
配置 MyBatis-Plus:
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置类
*
* @author ken
*/
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加数据库分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 添加自定义的ThreadLocal分页拦截器
interceptor.addInnerInterceptor(new ThreadLocalPaginationInterceptor());
return interceptor;
}
}
使用示例:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 产品控制器,演示分页查询
*
* @author ken
*/
@RestController
@RequestMapping("/products")
@Slf4j
@Tag(name = "产品接口", description = "产品相关的CRUD操作")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
@Operation(summary = "分页查询产品", description = "根据条件分页查询产品列表")
public IPage<Product> getProducts(
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") int pageNum,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
@Parameter(description = "产品名称模糊查询") @RequestParam(required = false) String name) {
try {
// 设置分页参数到ThreadLocal
PageContextHolder.setPageParam(pageNum, pageSize);
// 调用服务层查询,无需传递分页参数
return productService.findProductsByName(name);
} finally {
// 清除分页参数
PageContextHolder.clear();
}
}
}
/**
* 产品服务类
*
* @author ken
*/
@org.springframework.stereotype.Service
@Slf4j
public class ProductService {
private final ProductMapper productMapper;
public ProductService(ProductMapper productMapper) {
this.productMapper = productMapper;
}
/**
* 根据名称分页查询产品
*
* @param name 产品名称,可为null
* @return 分页查询结果
*/
public IPage<Product> findProductsByName(String name) {
// 创建查询条件
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (org.springframework.util.StringUtils.hasText(name)) {
queryWrapper.like("name", name);
}
// 执行查询,分页参数由ThreadLocal提供
List<Product> products = productMapper.selectList(queryWrapper);
// 获取总条数(实际应用中可能需要单独查询)
long total = productMapper.selectCount(queryWrapper);
// 构建分页结果
PageContextHolder.PageParam pageParam = PageContextHolder.getPageParam();
Page<Product> page = new Page<>(pageParam.getPageNum(), pageParam.getPageSize(), total);
page.setRecords(products);
return page;
}
}
/**
* 产品Mapper接口
*
* @author ken
*/
public interface ProductMapper extends com.baomidou.mybatisplus.core.mapper.BaseMapper<Product> {
// 继承BaseMapper,无需额外方法
}
/**
* 产品实体类
*
* @author ken
*/
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private double price;
private int stock;
private String description;
// 其他字段...
}
这种方式的优势在于:
在 AOP(面向切面编程)中,我们经常需要在切面和目标方法之间传递参数。ThreadLocal 可以作为一种便捷的传递方式,尤其是在复杂的切面逻辑中。
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 日志追踪上下文工具类
*
* @author ken
*/
@Slf4j
public class LogTraceContextHolder {
/**
* ThreadLocal实例,存储日志追踪上下文
*/
private static final ThreadLocal<LogTraceContext> LOG_TRACE_CONTEXT = ThreadLocal.withInitial(LogTraceContext::new);
/**
* 初始化日志追踪上下文
*
* @param methodName 方法名
* @param params 参数列表
*/
public static void init(String methodName, Object[] params) {
LogTraceContext context = new LogTraceContext();
context.setTraceId(generateTraceId());
context.setStartTime(LocalDateTime.now());
context.setMethodName(methodName);
context.setParams(params);
LOG_TRACE_CONTEXT.set(context);
log.debug("初始化日志追踪上下文: {}", context.getTraceId());
}
/**
* 设置方法执行结果
*
* @param result 方法执行结果
*/
public static void setResult(Object result) {
LogTraceContext context = LOG_TRACE_CONTEXT.get();
if (!ObjectUtils.isEmpty(context)) {
context.setResult(result);
context.setEndTime(LocalDateTime.now());
LOG_TRACE_CONTEXT.set(context);
log.debug("设置方法执行结果: {}", context.getTraceId());
} else {
log.warn("尝试设置方法执行结果,但日志追踪上下文未初始化");
}
}
/**
* 设置方法执行异常
*
* @param throwable 异常对象
*/
public static void setException(Throwable throwable) {
LogTraceContext context = LOG_TRACE_CONTEXT.get();
if (!ObjectUtils.isEmpty(context)) {
context.setThrowable(throwable);
context.setEndTime(LocalDateTime.now());
LOG_TRACE_CONTEXT.set(context);
log.debug("设置方法执行异常: {}", context.getTraceId());
} else {
log.warn("尝试设置方法执行异常,但日志追踪上下文未初始化");
}
}
/**
* 获取当前日志追踪上下文
*
* @return 当前日志追踪上下文
*/
public static LogTraceContext getContext() {
return LOG_TRACE_CONTEXT.get();
}
/**
* 清除当前日志追踪上下文
*/
public static void clear() {
LogTraceContext context = LOG_TRACE_CONTEXT.get();
if (!ObjectUtils.isEmpty(context)) {
log.debug("清除日志追踪上下文: {}", context.getTraceId());
} else {
log.debug("清除日志追踪上下文: 未初始化");
}
LOG_TRACE_CONTEXT.remove();
}
/**
* 生成追踪ID
*
* @return 追踪ID
*/
private static String generateTraceId() {
return "log_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
}
/**
* 日志追踪上下文类
*/
@Getter
@Setter
public static class LogTraceContext {
private String traceId; // 追踪ID
private LocalDateTime startTime; // 方法开始时间
private LocalDateTime endTime; // 方法结束时间
private String methodName; // 方法名
private Object[] params; // 方法参数
private Object result; // 方法返回结果
private Throwable throwable; // 异常信息
}
}
定义 AOP 切面:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 方法执行日志切面
*
* @author ken
*/
@Aspect
@Component
@Slf4j
public class MethodLogAspect {
/**
* 定义切入点:所有Service层的方法
*/
@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethodPointcut() {
// 切入点定义,无实际代码
}
/**
* 环绕通知,记录方法执行日志
*
* @param joinPoint 连接点
* @return 方法执行结果
* @throws Throwable 方法执行过程中抛出的异常
*/
@Around("serviceMethodPointcut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法名
String methodName = joinPoint.getSignature().toShortString();
try {
// 初始化日志追踪上下文
LogTraceContextHolder.init(methodName, joinPoint.getArgs());
// 执行目标方法
Object result = joinPoint.proceed();
// 设置方法执行结果
LogTraceContextHolder.setResult(result);
// 记录成功日志
logSuccessLog();
return result;
} catch (Throwable throwable) {
// 设置方法执行异常
LogTraceContextHolder.setException(throwable);
// 记录异常日志
logErrorLog();
// 重新抛出异常
throw throwable;
} finally {
// 清除上下文
LogTraceContextHolder.clear();
}
}
/**
* 记录成功执行的日志
*/
private void logSuccessLog() {
LogTraceContextHolder.LogTraceContext context = LogTraceContextHolder.getContext();
if (context == null) {
log.warn("无法记录成功日志,上下文为空");
return;
}
// 计算执行时间
long duration = Duration.between(context.getStartTime(), context.getEndTime()).toMillis();
log.info("[方法执行成功] 追踪ID: {}, 方法名: {}, 执行时间: {}ms, 参数: {}, 返回值: {}",
context.getTraceId(),
context.getMethodName(),
duration,
context.getParams(),
context.getResult());
}
/**
* 记录执行异常的日志
*/
private void logErrorLog() {
LogTraceContextHolder.LogTraceContext context = LogTraceContextHolder.getContext();
if (context == null) {
log.warn("无法记录异常日志,上下文为空");
return;
}
// 计算执行时间
long duration = Duration.between(context.getStartTime(), context.getEndTime()).toMillis();
log.error("[方法执行异常] 追踪ID: {}, 方法名: {}, 执行时间: {}ms, 参数: {}, 异常信息: {}",
context.getTraceId(),
context.getMethodName(),
duration,
context.getParams(),
context.getThrowable().getMessage(),
context.getThrowable());
}
}
这种方式通过 ThreadLocal 在切面的不同阶段(前置、执行、后置)之间传递数据,避免了使用方法参数传递的麻烦,使切面逻辑更加清晰。
许多优秀的 Java 框架都广泛使用了 ThreadLocal 来解决线程安全和上下文传递问题。了解这些框架的实现方式,可以帮助我们更好地理解 ThreadLocal 的应用场景。
Spring 框架中多处使用了 ThreadLocal,其中最典型的是:
以 RequestContextHolder 为例,其简化实现原理如下:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
/**
* 请求上下文持有类,简化版实现
*
* @author ken
*/
@Slf4j
public class RequestContextHolder {
/**
* 存储当前线程的请求上下文
*/
private static final ThreadLocal<ServletRequestAttributes> requestAttributesHolder =
new ThreadLocal<>();
/**
* 设置当前请求的上下文
*
* @param request 请求对象
* @param response 响应对象
*/
public static void setRequestAttributes(HttpServletRequest request, HttpServletResponse response) {
if (ObjectUtils.isEmpty(request)) {
log.error("请求对象不能为空");
throw new IllegalArgumentException("请求对象不能为空");
}
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
requestAttributesHolder.set(attributes);
log.debug("设置请求上下文: {}", request.getRequestURI());
}
/**
* 获取当前请求对象
*
* @return 当前请求对象,如果不存在则返回null
*/
public static HttpServletRequest getRequest() {
ServletRequestAttributes attributes = requestAttributesHolder.get();
if (ObjectUtils.isEmpty(attributes)) {
log.debug("当前线程没有请求上下文");
return null;
}
return attributes.getRequest();
}
/**
* 获取当前响应对象
*
* @return 当前响应对象,如果不存在则返回null
*/
public static HttpServletResponse getResponse() {
ServletRequestAttributes attributes = requestAttributesHolder.get();
if (ObjectUtils.isEmpty(attributes)) {
log.debug("当前线程没有请求上下文");
return null;
}
return attributes.getResponse();
}
/**
* 清除当前请求上下文
*/
public static void resetRequestAttributes() {
ServletRequestAttributes attributes = requestAttributesHolder.get();
if (!ObjectUtils.isEmpty(attributes)) {
log.debug("清除请求上下文: {}", attributes.getRequest().getRequestURI());
} else {
log.debug("清除请求上下文: 当前线程没有请求上下文");
}
requestAttributesHolder.remove();
}
/**
* 请求属性包装类
*/
public static class ServletRequestAttributes {
private final HttpServletRequest request;
private final HttpServletResponse response;
public ServletRequestAttributes(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
public HttpServletRequest getRequest() {
return request;
}
public HttpServletResponse getResponse() {
return response;
}
}
}
在实际应用中,我们可以在任何地方通过 RequestContextHolder 获取当前请求对象,而无需通过方法参数传递:
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 演示如何使用RequestContextHolder
*
* @author ken
*/
@RestController
@RequestMapping("/demo")
@Slf4j
public class DemoController {
private final DemoService demoService;
public DemoController(DemoService demoService) {
this.demoService = demoService;
}
@GetMapping("/request-info")
public String getRequestInfo() {
// 可以直接获取请求信息
HttpServletRequest request = RequestContextHolder.getRequest();
log.info("Controller层获取请求信息: {}", request.getRequestURI());
// 调用服务层
demoService.processRequest();
return "Request info processed";
}
}
/**
* 演示服务类
*
* @author ken
*/
@org.springframework.stereotype.Service
@Slf4j
public class DemoService {
public void processRequest() {
// 在服务层也可以直接获取请求信息,无需通过参数传递
HttpServletRequest request = RequestContextHolder.getRequest();
log.info("Service层获取请求信息: {}", request.getRemoteAddr());
// 可以继续传递到更深层次...
}
}
MyBatis 中也广泛使用了 ThreadLocal,例如:
MyBatis 一级缓存的简化实现原理:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.util.HashMap;
import java.util.Map;
/**
* MyBatis一级缓存的简化实现
*
* @author ken
*/
@Slf4j
public class LocalCache {
/**
* 存储当前线程的缓存
*/
private static final ThreadLocal<Map<String, Object>> LOCAL_CACHE = ThreadLocal.withInitial(HashMap::new);
/**
* 默认缓存大小
*/
private static final int DEFAULT_CACHE_SIZE = 1024;
/**
* 从缓存中获取数据
*
* @param key 缓存键
* @return 缓存值,如果不存在则返回null
*/
public static Object getObject(String key) {
if (!ObjectUtils.isEmpty(key)) {
Object value = LOCAL_CACHE.get().get(key);
if (value != null) {
log.debug("从本地缓存获取数据,key: {}", key);
}
return value;
}
return null;
}
/**
* 向缓存中存入数据
*
* @param key 缓存键
* @param value 缓存值
*/
public static void putObject(String key, Object value) {
if (!ObjectUtils.isEmpty(key) && value != null) {
Map<String, Object> cache = LOCAL_CACHE.get();
// 如果缓存大小超过阈值,清除缓存
if (cache.size() >= DEFAULT_CACHE_SIZE) {
clear();
}
cache.put(key, value);
log.debug("向本地缓存存入数据,key: {}", key);
}
}
/**
* 从缓存中移除数据
*
* @param key 缓存键
* @return 被移除的值,如果不存在则返回null
*/
public static Object removeObject(String key) {
if (!ObjectUtils.isEmpty(key)) {
log.debug("从本地缓存移除数据,key: {}", key);
return LOCAL_CACHE.get().remove(key);
}
return null;
}
/**
* 清除当前线程的缓存
*/
public static void clear() {
Map<String, Object> cache = LOCAL_CACHE.get();
log.debug("清除本地缓存,大小: {}", cache.size());
cache.clear();
}
/**
* 生成缓存键
*
* @param statementId Mapper语句ID
* @param params 参数
* @return 缓存键
*/
public static String generateKey(String statementId, Object... params) {
StringBuilder keyBuilder = new StringBuilder(statementId);
keyBuilder.append(":");
if (!ObjectUtils.isEmpty(params)) {
for (Object param : params) {
keyBuilder.append(param).append(",");
}
}
return keyBuilder.toString();
}
}
这种线程级别的缓存确保了在同一个事务中,相同的查询不会重复访问数据库,提高了查询效率,同时又避免了多线程之间的缓存共享问题。
ThreadLocal 最常见的问题是可能导致内存泄漏。这是因为 ThreadLocalMap 中的 Entry 使用弱引用(WeakReference)指向 ThreadLocal 实例,而 value 是强引用。
当 ThreadLocal 实例被回收后,Entry 的 key 变为 null,但 value 仍然存在,并且被 ThreadLocalMap 引用,如果线程长时间存活(如线程池中的核心线程),这些 value 就会一直占用内存,导致内存泄漏。

解决方案:
正确的使用模式:
// 正确的ThreadLocal使用模式
try {
// 设置ThreadLocal值
threadLocal.set(value);
// 业务逻辑
doBusiness();
} finally {
// 无论如何都要清除
threadLocal.remove();
}
在线程池环境中,线程会被复用,如果前一个任务没有清除 ThreadLocal 中的值,下一个任务可能会意外获取到前一个任务的值,导致数据错乱。
示例代码:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池环境下ThreadLocal的陷阱演示
*
* @author ken
*/
@Slf4j
public class ThreadLocalThreadPoolTrap {
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2);
@Test
public void testThreadPoolTrap() throws InterruptedException {
// 提交第一个任务
EXECUTOR.submit(() -> {
try {
THREAD_LOCAL.set("Task 1 Value");
log.info("任务1设置的值: {}", THREAD_LOCAL.get());
} finally {
// 注释掉remove()调用,模拟忘记清除的情况
// THREAD_LOCAL.remove();
}
});
// 等待第一个任务完成
Thread.sleep(1000);
// 提交第二个任务
EXECUTOR.submit(() -> {
try {
// 第二个任务没有设置值,但可能获取到第一个任务的值
log.info("任务2获取到的值: {}", THREAD_LOCAL.get());
} finally {
THREAD_LOCAL.remove();
}
});
// 关闭线程池
EXECUTOR.shutdown();
}
}
运行结果可能如下:
任务1设置的值: Task 1 Value
任务2获取到的值: Task 1 Value // 意外获取到了前一个任务的值
解决方案:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
/**
* 带有ThreadLocal清理功能的线程池
*
* @author ken
*/
@Slf4j
public class ThreadLocalCleanupExecutor extends ThreadPoolExecutor {
public ThreadLocalCleanupExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
log.debug("线程 {} 开始执行任务", t.getName());
// 可以在这里初始化ThreadLocal
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
log.debug("线程 {} 完成任务执行", Thread.currentThread().getName());
// 清理所有ThreadLocal
clearThreadLocals();
}
/**
* 清除当前线程的所有ThreadLocal
*/
private void clearThreadLocals() {
try {
// 通过反射获取ThreadLocalMap
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocalMap = threadLocalsField.get(thread);
if (threadLocalMap != null) {
// 获取ThreadLocalMap的clear方法
java.lang.reflect.Method clearMethod = threadLocalMap.getClass().getDeclaredMethod("clear");
clearMethod.setAccessible(true);
clearMethod.invoke(threadLocalMap);
log.debug("已清除线程 {} 的所有ThreadLocal", thread.getName());
}
} catch (Exception e) {
log.error("清除ThreadLocal失败", e);
}
}
/**
* 创建一个带有ThreadLocal清理功能的固定大小线程池
*
* @param nThreads 线程数量
* @return 线程池实例
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadLocalCleanupExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
}
}
需要明确的是,ThreadLocal不能解决线程安全问题,它只是一种线程封闭机制。
ThreadLocal 与线程安全的区别:

ThreadLocal 适用于:
线程同步适用于:
ThreadLocal 是 Java 并发编程中一个强大而灵活的工具,它通过为每个线程提供独立的变量副本,实现了线程封闭,避免了许多复杂的同步问题。
本文详细介绍了 ThreadLocal 的 6 大应用场景:
同时,我们也深入探讨了 ThreadLocal 的底层实现原理和使用中的注意事项,特别是内存泄漏问题和线程池环境下的陷阱,以及相应的解决方案。
正确使用 ThreadLocal 可以显著提高代码的简洁性和性能,但如果使用不当,也会带来难以排查的问题。记住 "用完即清理" 的原则,始终在 finally 块中调用 remove () 方法,这是避免大多数 ThreadLocal 相关问题的关键。