首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 ThreadLocal 到并发王者:解锁线程私有变量的 6 大实战场景

从 ThreadLocal 到并发王者:解锁线程私有变量的 6 大实战场景

作者头像
果酱带你啃java
发布2026-04-14 12:53:58
发布2026-04-14 12:53:58
850
举报

前言

在 Java 并发编程的世界里,ThreadLocal 是一个看似简单却威力无穷的工具。它就像一位隐形的助手,默默地为每个线程维护着一份专属的数据副本,让我们在多线程环境下无需为共享变量的同步问题头疼。

然而,很多开发者对 ThreadLocal 的理解仅仅停留在 "线程本地变量" 这个表层概念,并没有真正发挥它的潜力,甚至在使用中频繁踩坑。你是否也曾遇到过这样的困惑:

  • 为什么使用 ThreadLocal 后依然出现线程安全问题?
  • ThreadLocal 和 synchronized 到底有什么本质区别?
  • 如何正确地在 Web 应用中使用 ThreadLocal 存储用户上下文?
  • ThreadLocal 可能导致的内存泄漏是怎么回事?

本文将带你全面深入 ThreadLocal 的世界,从底层原理到实战应用,详细解析 6 大常见应用场景,并通过可运行的代码示例展示最佳实践。无论你是刚接触并发编程的新手,还是希望提升并发处理能力的资深开发者,都能从本文中获得实用的知识和技巧。

一、ThreadLocal 核心原理

在探讨应用场景之前,我们首先需要深入理解 ThreadLocal 的工作原理。只有知其然且知其所以然,才能在实际开发中灵活运用并避免常见陷阱。

1.1 ThreadLocal 是什么

ThreadLocal,即线程本地变量,是 Java 提供的一种线程封闭机制。它允许我们创建一个变量,该变量在每个线程中都有一个独立的副本,线程对变量的操作只会影响到自己的副本,不会影响其他线程。

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

代码语言:javascript
复制

1.2 ThreadLocal 的底层实现

在 JDK 17 中,ThreadLocal 的实现机制如下:

每个 Thread 对象内部都维护着一个 ThreadLocalMap 对象,这个 Map 的 key 是 ThreadLocal 实例本身,value 是线程的变量副本。

代码语言:javascript
复制
// 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;
        }
    }
}
代码语言:javascript
复制

ThreadLocal 的工作流程:

代码语言:javascript
复制

这种实现方式的优点:

  1. 每个线程只需要访问自己的 ThreadLocalMap,避免了多线程竞争
  2. 当线程销毁时,其对应的 ThreadLocalMap 也会被销毁,减少内存占用
  3. 使用弱引用存储 key,有助于在 ThreadLocal 不再使用时自动回收内存

1.3 ThreadLocal 的核心方法

ThreadLocal 类提供了几个核心方法:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

二、ThreadLocal 的常见应用场景

2.1 保存线程上下文信息

在多层调用的场景中, ThreadLocal 可以用来传递上下文信息,避免通过方法参数层层传递的麻烦。最典型的例子就是 Web 应用中的用户登录信息传递。

2.1.1 Web 应用中的用户上下文

在 Web 开发中,用户登录后,其身份信息需要在整个请求处理过程中被访问到,从 Controller 到 Service 再到 Dao 层。使用 ThreadLocal 可以优雅地实现这一点。

首先,创建一个用户上下文工具类:

代码语言:javascript
复制
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;
        // 可以根据需要添加更多字段
    }
}
代码语言:javascript
复制

然后,在拦截器中设置和清除用户上下文:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

配置拦截器:

代码语言:javascript
复制
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("/**");
    }
}
代码语言:javascript
复制

在 Controller、Service、Dao 等各层中使用:

代码语言:javascript
复制
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作为查询条件或记录操作人
    }
}
代码语言:javascript
复制

这种方式的优势:

  1. 避免了在方法参数中传递用户信息,简化了方法签名
  2. 各层可以按需获取用户信息,提高了代码的灵活性
  3. 确保了用户信息在同一请求中的一致性
2.1.2 分布式追踪中的链路 ID 传递

在分布式系统中,为了追踪一个请求的完整链路,通常会使用一个唯一的链路 ID(Trace ID)。ThreadLocal 可以用来在当前服务的各个组件中传递这个 Trace ID。

代码语言:javascript
复制
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; // 父服务的调用标识
    }
}
代码语言:javascript
复制

在拦截器中初始化和传递 Trace ID:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

通过这种方式,我们可以在日志中包含 Trace ID,从而轻松追踪一个请求在分布式系统中的完整路径。

2.2 避免线程安全问题

在多线程环境下,某些工具类可能因为存在状态而导致线程安全问题。使用 ThreadLocal 可以为每个线程提供独立的工具类实例,从而避免同步和线程安全问题。

2.2.1 日期格式化工具

SimpleDateFormat 是 Java 中常用的日期格式化类,但它是非线程安全的。在多线程环境下共享一个 SimpleDateFormat 实例会导致各种奇怪的问题。

使用 ThreadLocal 为每个线程提供独立的 SimpleDateFormat 实例:

代码语言:javascript
复制
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实例");
    }
}
代码语言:javascript
复制

测试线程安全性:

代码语言:javascript
复制
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("所有线程执行完毕");
    }
}
代码语言:javascript
复制

这种方式相比每次创建新的 SimpleDateFormat 实例性能更好,因为避免了频繁创建和销毁对象的开销;同时又比共享一个实例更安全,不需要同步操作。

2.2.2 数据库连接管理

在 JDBC 中,数据库连接(Connection)是线程不安全的,通常每个线程应该使用自己的连接。ThreadLocal 可以用来管理连接的生命周期。

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
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();
        }
    }
}
代码语言:javascript
复制

这种方式确保了在同一事务中的所有数据库操作使用的是同一个连接,并且每个线程都有自己独立的连接,避免了多线程共享连接导致的问题。

2.3 事务管理

在事务管理中,ThreadLocal 的作用至关重要。它确保了在一个事务中,所有的数据库操作都使用同一个连接,并且这个连接不会被其他线程干扰。

2.3.1 自定义事务管理器

虽然实际开发中我们通常使用 Spring 的声明式事务管理,但了解其底层实现原理有助于我们更好地理解事务管理的本质。

代码语言:javascript
复制
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;
        }
    }
}
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

Spring 的事务管理也是基于类似的原理,通过 ThreadLocal 存储当前事务的连接,并在整个事务过程中复用该连接,确保了事务的 ACID 特性。

2.4 分页查询中的参数传递

在复杂的分页查询中,分页参数(页码、每页条数等)需要在多个方法之间传递。使用 ThreadLocal 可以简化参数传递,尤其是在无法修改方法签名的情况下。

2.4.1 分页上下文管理
代码语言:javascript
复制
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;
        }
    }
}
代码语言:javascript
复制

在 MyBatis-Plus 中使用:

首先,创建分页拦截器:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

配置 MyBatis-Plus:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

使用示例:

代码语言:javascript
复制
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;
    // 其他字段...
}
代码语言:javascript
复制

这种方式的优势在于:

  1. 避免了在方法调用链中传递分页参数,简化了方法签名
  2. 可以在任意层级获取分页参数,提高了代码的灵活性
  3. 特别适合在无法修改第三方库方法签名的情况下添加分页功能

2.5 AOP 中的参数传递

在 AOP(面向切面编程)中,我们经常需要在切面和目标方法之间传递参数。ThreadLocal 可以作为一种便捷的传递方式,尤其是在复杂的切面逻辑中。

2.5.1 日志追踪中的参数传递
代码语言:javascript
复制
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; // 异常信息
    }
}
代码语言:javascript
复制

定义 AOP 切面:

代码语言:javascript
复制
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());
    }
}
代码语言:javascript
复制

这种方式通过 ThreadLocal 在切面的不同阶段(前置、执行、后置)之间传递数据,避免了使用方法参数传递的麻烦,使切面逻辑更加清晰。

2.6 框架中的 ThreadLocal 应用

许多优秀的 Java 框架都广泛使用了 ThreadLocal 来解决线程安全和上下文传递问题。了解这些框架的实现方式,可以帮助我们更好地理解 ThreadLocal 的应用场景。

2.6.1 Spring 中的 ThreadLocal 应用

Spring 框架中多处使用了 ThreadLocal,其中最典型的是:

  1. RequestContextHolder存储当前请求的上下文信息
  2. TransactionSynchronizationManager管理事务同步信息
  3. LocaleContextHolder存储当前线程的国际化信息

以 RequestContextHolder 为例,其简化实现原理如下:

代码语言:javascript
复制
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;
        }
    }
}
代码语言:javascript
复制

在实际应用中,我们可以在任何地方通过 RequestContextHolder 获取当前请求对象,而无需通过方法参数传递:

代码语言:javascript
复制
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());

        // 可以继续传递到更深层次...
    }
}
代码语言:javascript
复制

2.6.2 MyBatis 中的 ThreadLocal 应用

MyBatis 中也广泛使用了 ThreadLocal,例如:

  1. LocalCache一级缓存的实现,基于 ThreadLocal
  2. Executor每个线程有自己的 Executor 实例

MyBatis 一级缓存的简化实现原理:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

这种线程级别的缓存确保了在同一个事务中,相同的查询不会重复访问数据库,提高了查询效率,同时又避免了多线程之间的缓存共享问题。

三、ThreadLocal 的注意事项和最佳实践

3.1 内存泄漏问题

ThreadLocal 最常见的问题是可能导致内存泄漏。这是因为 ThreadLocalMap 中的 Entry 使用弱引用(WeakReference)指向 ThreadLocal 实例,而 value 是强引用。

当 ThreadLocal 实例被回收后,Entry 的 key 变为 null,但 value 仍然存在,并且被 ThreadLocalMap 引用,如果线程长时间存活(如线程池中的核心线程),这些 value 就会一直占用内存,导致内存泄漏。

代码语言:javascript
复制

解决方案:

  1. 使用完 ThreadLocal 后及时调用 remove () 方法:这是最有效的方式,可以清除当前线程对应的 value。
  2. 避免使用静态 ThreadLocal:静态 ThreadLocal 的生命周期与类相同,更容易导致内存泄漏。
  3. 在线程池环境中特别注意:线程池中的线程会被复用,必须确保在每个任务执行完毕后清除 ThreadLocal。

正确的使用模式:

代码语言:javascript
复制
// 正确的ThreadLocal使用模式
try {
    // 设置ThreadLocal值
    threadLocal.set(value);
    // 业务逻辑
    doBusiness();
} finally {
    // 无论如何都要清除
    threadLocal.remove();
}
代码语言:javascript
复制

3.2 线程池环境中的陷阱

在线程池环境中,线程会被复用,如果前一个任务没有清除 ThreadLocal 中的值,下一个任务可能会意外获取到前一个任务的值,导致数据错乱。

示例代码:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

运行结果可能如下:

代码语言:javascript
复制
任务1设置的值: Task 1 Value
任务2获取到的值: Task 1 Value  // 意外获取到了前一个任务的值
代码语言:javascript
复制

解决方案:

  1. 始终在 finally 块中调用 remove () 方法,确保无论任务是否正常执行,都能清除 ThreadLocal 的值。
  2. 使用自定义线程池,在任务执行前后进行拦截处理:
代码语言:javascript
复制
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<>());
    }
}
代码语言:javascript
复制

3.3 ThreadLocal 与线程安全的关系

需要明确的是,ThreadLocal不能解决线程安全问题,它只是一种线程封闭机制。

ThreadLocal 与线程安全的区别:

代码语言:javascript
复制

ThreadLocal 适用于:

  • 每个线程需要自己的独立实例,且实例需要在多个方法中使用
  • 避免参数传递的场景

线程同步适用于:

  • 多个线程需要共享同一个实例
  • 需要保证数据一致性的场景

3.4 ThreadLocal 的最佳实践总结

  1. 始终在 finally 块中调用 remove () 方法,这是避免内存泄漏和数据错乱的关键。
  2. 使用 private static 修饰 ThreadLocal 实例,避免被意外修改或继承。
  3. 使用 withInitial () 方法初始化(JDK 8+),使代码更简洁。
  4. 避免存储大量数据,ThreadLocal 存储的数据会随着线程的生命周期存在,大量数据会导致内存占用过高。
  5. 在使用线程池时格外小心,确保线程复用不会导致数据污染。
  6. 不要将 ThreadLocal 作为全局变量的替代方案,它本质上是线程级别的变量,不适合存储全局共享数据。
  7. 使用 ThreadLocal 包装非线程安全的对象,如 SimpleDateFormat、Random 等。
  8. 避免在 ThreadLocal 中存储可变对象的引用,如果必须存储,要确保这些对象不会被其他线程修改。

四、总结

ThreadLocal 是 Java 并发编程中一个强大而灵活的工具,它通过为每个线程提供独立的变量副本,实现了线程封闭,避免了许多复杂的同步问题。

本文详细介绍了 ThreadLocal 的 6 大应用场景:

  1. 保存线程上下文信息:如 Web 应用中的用户信息、分布式追踪中的链路 ID 等,避免了参数的层层传递。
  2. 避免线程安全问题:为每个线程提供非线程安全对象(如 SimpleDateFormat)的独立实例,既保证了线程安全,又提高了性能。
  3. 事务管理:确保在一个事务中所有操作使用同一个数据库连接,是事务 ACID 特性的基础。
  4. 分页查询中的参数传递:简化了分页参数在多层方法调用中的传递,提高了代码的灵活性。
  5. AOP 中的参数传递:在切面的不同阶段传递数据,使切面逻辑更加清晰。
  6. 框架中的应用:如 Spring 的 RequestContextHolder、MyBatis 的一级缓存等,ThreadLocal 在这些框架中发挥了关键作用。

同时,我们也深入探讨了 ThreadLocal 的底层实现原理和使用中的注意事项,特别是内存泄漏问题和线程池环境下的陷阱,以及相应的解决方案。

正确使用 ThreadLocal 可以显著提高代码的简洁性和性能,但如果使用不当,也会带来难以排查的问题。记住 "用完即清理" 的原则,始终在 finally 块中调用 remove () 方法,这是避免大多数 ThreadLocal 相关问题的关键。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、ThreadLocal 核心原理
    • 1.1 ThreadLocal 是什么
    • 1.2 ThreadLocal 的底层实现
    • 1.3 ThreadLocal 的核心方法
  • 二、ThreadLocal 的常见应用场景
    • 2.1 保存线程上下文信息
      • 2.1.1 Web 应用中的用户上下文
      • 2.1.2 分布式追踪中的链路 ID 传递
    • 2.2 避免线程安全问题
      • 2.2.1 日期格式化工具
      • 2.2.2 数据库连接管理
    • 2.3 事务管理
      • 2.3.1 自定义事务管理器
    • 2.4 分页查询中的参数传递
      • 2.4.1 分页上下文管理
    • 2.5 AOP 中的参数传递
      • 2.5.1 日志追踪中的参数传递
    • 2.6 框架中的 ThreadLocal 应用
      • 2.6.1 Spring 中的 ThreadLocal 应用
      • 2.6.2 MyBatis 中的 ThreadLocal 应用
  • 三、ThreadLocal 的注意事项和最佳实践
    • 3.1 内存泄漏问题
    • 3.2 线程池环境中的陷阱
    • 3.3 ThreadLocal 与线程安全的关系
    • 3.4 ThreadLocal 的最佳实践总结
  • 四、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档