面试必知必会(9):ThreadLocal

Java面试系列文章

面试必知必会(1):线程状态和创建方式

面试必知必会(2):线程池原理

面试必知必会(3):synchronized底层原理

面试必知必会(4):volatile关键字

面试必知必会(5):CAS与原子类

面试必知必会(6):Lock接口及实现类

面试必知必会(7):多线程AQS

面试必知必会(8):CountDownLatch、CyclicBarrier、Semaphore、Exchanger

面试必知必会(9):ThreadLocal


目录

一、什么是 ThreadLocal?

ThreadLocal是Java.lang包下的一个工具类,官方文档对其描述如下:"ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。"

  • 线程私有:每个线程持有该变量的独立副本,线程对副本的所有操作(读/写)均不会影响其他线程
  • 无需加锁:不同于synchronized、Lock的"串行执行"思路,ThreadLocal通过"空间换时间"实现线程安全,无锁竞争,并发效率更高
  • 上下文传递:可在同一线程的不同方法、不同组件(如Controller→Service→DAO)中共享数据,无需通过方法参数层层传递,简化代码逻辑
  • 生命周期绑定:变量副本的生命周期与线程一致,线程终止后,副本会被回收(若未出现内存泄漏)

基本用法示例:

java 复制代码
public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int value = threadLocalValue.get();
            System.out.println(Thread.currentThread().getName() + " 初始值: " + value);
            threadLocalValue.set(value + 1);
            System.out.println(Thread.currentThread().getName() + " 更新后: " + threadLocalValue.get());
        };

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

输出结果:

java 复制代码
Thread-1 初始值: 0
Thread-1 更新后: 1
Thread-2 初始值: 0
Thread-2 更新后: 1

二、底层原理:ThreadLocal是如何实现线程隔离的?

要真正吃透ThreadLocal,必须搞懂其底层实现逻辑。很多开发者误以为ThreadLocal是自己存储了线程的变量副本,实则不然------ThreadLocal本身不存储任何数据,它只是一个"工具人",负责帮线程找到自己的变量副本

ThreadLocal的底层隔离机制,依赖于「ThreadThreadLocalThreadLocalMap」三个核心对象的配合,三者的关系是理解原理的关键:

Thread(线程) → 持有 ThreadLocalMap(哈希表) → ThreadLocalMap 中存储 <ThreadLocal(Key), 变量副本(Value)> 键值对

1、三个核心对象的角色拆解

1.1、Thread:线程对象

每个Thread线程对象内部,都维护着一个成员变量:ThreadLocalMap类型的threadLocals,初始值为null。

源码片段(JDK 8):

java 复制代码
public class Thread implements Runnable {
    // 线程持有的ThreadLocalMap,用于存储线程本地变量副本
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // 另一个用于继承的ThreadLocalMap(父子线程传递,后续讲解)
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    // 其他代码省略...
}

关键点:threadLocals是Thread的私有变量,每个线程都有自己独立的threadLocals,这是线程隔离的基础------不同线程的threadLocals互不干扰,存储的变量副本自然也相互独立。

1.2、ThreadLocal:工具类

ThreadLocal本身不存储数据,它的核心作用是:作为ThreadLocalMap中的Key,帮线程找到对应的Value(变量副本)

补充细节:ThreadLocal对象通常被定义为private static类型,这是因为:

  • static修饰:保证整个应用中,该ThreadLocal对象唯一,避免创建多个ThreadLocal实例导致的资源浪费和逻辑混乱
  • private修饰:避免被其他类篡改,保证线程本地变量的安全性

1.3、ThreadLocalMap:线程本地变量的"存储容器"

ThreadLocalMap是ThreadLocal的静态内部类,本质是一个「自定义的哈希表」,专门用于存储线程本地变量副本

ThreadLocalMap的核心特点:

  • 结构简单:采用数组+线性探测法解决哈希冲突(HashMap采用数组+链表/红黑树),因为ThreadLocalMap的Key(ThreadLocal)数量通常较少,线性探测法效率足够高,且实现更简洁
  • Key是弱引用:ThreadLocalMap中的Key(ThreadLocal对象)采用弱引用存储,这是为了避免内存泄漏(后续详细讲解)
  • Value是强引用:ThreadLocalMap中的Value(变量副本)采用强引用存储,这也是导致内存泄漏的潜在风险点
  • 归属于线程:每个ThreadLocalMap只属于一个线程,仅当前线程可访问,无需考虑并发安全问题(无需加锁)

2、核心原理流程图(直观理解)

用一句话概括整个流程:当线程调用ThreadLocal的set()方法存储数据时,数据最终存储在当前线程自己的ThreadLocalMap中;调用get()方法获取数据时,也只能从当前线程的ThreadLocalMap中,以当前ThreadLocal为Key取出对应的Value。

set(Value)流程

  • ① 获取当前线程(Thread.currentThread())
  • ② 获取当前线程的ThreadLocalMap(threadLocals)
  • ③ 若ThreadLocalMap不存在,则创建一个新的ThreadLocalMap
  • ④ 以当前ThreadLocal对象为Key,传入的Value为值,存入ThreadLocalMap
  • ⑤ 完成存储,后续当前线程可通过get()方法获取该Value

get()流程

  • ① 获取当前线程(Thread.currentThread())
  • ② 获取当前线程的ThreadLocalMap(threadLocals)
  • ③ 若ThreadLocalMap不存在,或Map中没有当前ThreadLocal对应的Key,则调用initialValue()方法初始化默认值(默认返回null),并将初始化后的值存入Map
  • ④ 若存在对应的Key,则返回对应的Value(变量副本)
  • ⑤ 其他线程调用get()方法时,会获取自己线程的ThreadLocalMap,无法获取当前线程的Value

3、补充:InheritableThreadLocal(父子线程数据传递)

普通的ThreadLocal有一个局限性:父子线程间的数据无法传递。比如,主线程中通过ThreadLocal存储了用户信息,子线程中调用get()方法会返回null,因为子线程的threadLocals是独立的,不会继承主线程的threadLocals。

为了解决这个问题,JDK提供了ThreadLocal的子类------InheritableThreadLocal,它重写了ThreadLocal的相关方法,让子线程可以继承主线程的ThreadLocal数据。

java 复制代码
// InheritableThreadLocal继承自 ThreadLocal,重写了以下方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 重写此方法,在创建子线程时传递值
    protected T childValue(T parentValue) {
        return parentValue;
    }
}
  • 当父线程创建子线程时,会将 inheritableThreadLocals复制到子线程
  • 子线程的 init()方法会处理这个复制过程
  • 子线程可以修改自己的副本,不影响父线程
java 复制代码
public class InheritableThreadLocalDemo {
    // 1. 定义 InheritableThreadLocal
    private static final InheritableThreadLocal<String> inheritableThreadLocal = 
        new InheritableThreadLocal<>();
    
    // 2. 普通 ThreadLocal 对比
    private static final ThreadLocal<String> normalThreadLocal = 
        new ThreadLocal<>();
    
    public static void main(String[] args) throws InterruptedException {
        // 父线程设置值
        inheritableThreadLocal.set("父线程数据");
        normalThreadLocal.set("普通ThreadLocal数据");
        
        System.out.println("父线程 inheritableThreadLocal: " + 
                          inheritableThreadLocal.get());
        System.out.println("父线程 normalThreadLocal: " + 
                          normalThreadLocal.get());
        
        // 创建子线程
        Thread childThread = new Thread(() -> {
            // 子线程能获取到 inheritableThreadLocal 的值
            System.out.println("子线程 inheritableThreadLocal: " + 
                              inheritableThreadLocal.get());  // 输出:父线程数据
            
            // 子线程无法获取 normalThreadLocal 的值
            System.out.println("子线程 normalThreadLocal: " + 
                              normalThreadLocal.get());  // 输出:null
        });
        
        childThread.start();
        childThread.join();
    }
}

三、源码拆解:深入ThreadLocal的核心方法(JDK 8)

1、initialValue():初始化变量副本

该方法用于初始化线程本地变量的默认值,当线程调用get()方法时,若当前线程的ThreadLocalMap中没有对应的Key,会调用该方法生成默认值,并将其存入Map中。

源码片段:

java 复制代码
protected T initialValue() {
    // 默认返回null,开发者可重写该方法,自定义初始化逻辑
    return null;
}
  • 该方法是protected修饰的,允许开发者通过匿名内部类子类重写,实现自定义初始化
  • 该方法仅会被调用一次:当线程第一次调用get()方法,且Map中无对应Value时,才会调用;后续调用get()方法不会再触发
  • JDK 8提供了便捷方法withInitial(),可通过Lambda表达式快速创建ThreadLocal并指定初始化逻辑,无需重写initialValue():
java 复制代码
// 示例:初始化一个String类型的ThreadLocal,默认值为"default"
ThreadLocal<String> tl = ThreadLocal.withInitial(() -> "default");

2、set(T value):存储变量副本

该方法是ThreadLocal的核心方法,用于将变量副本存储到当前线程的ThreadLocalMap中。

源码片段:

java 复制代码
public void set(T value) {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 3. 若Map存在,直接存入键值对(this即当前ThreadLocal对象,作为Key)
    if (map != null)
        map.set(this, value);
    // 4. 若Map不存在,创建新的ThreadLocalMap并存入数据
    else
        createMap(t, value);
}

辅助方法1:getMap(Thread t)

  • 作用:获取指定线程的ThreadLocalMap(即线程的threadLocals成员变量)
java 复制代码
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; // 直接返回线程的threadLocals变量
}

辅助方法2:createMap(Thread t, T firstValue)

  • 作用:为指定线程创建一个新的ThreadLocalMap,并将第一个键值对(当前ThreadLocal为Key,firstValue为Value)存入Map
java 复制代码
void createMap(Thread t, T firstValue) {
    // 初始化ThreadLocalMap,并赋值给线程的threadLocals变量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set()方法的核心细节

  • Key是当前ThreadLocal对象(this),保证了每个ThreadLocal在Map中对应唯一的Value
  • Map的归属:ThreadLocalMap是线程的私有变量,因此set()方法无需加锁,线程安全
  • 覆盖逻辑:若当前线程的Map中已存在当前ThreadLocal对应的Key,再次调用set()方法会覆盖原有的Value

3、get():获取变量副本

该方法用于获取当前线程存储的变量副本,核心逻辑是"找当前线程的Map,再找Map中当前ThreadLocal对应的Value"。

源码片段:

java 复制代码
public T get() {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 3. 若Map存在,且存在当前ThreadLocal对应的Key,返回Value
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 4. 若Map不存在,或无对应Key,调用initialValue()初始化
    return setInitialValue();
}

辅助方法:setInitialValue()

  • 作用:初始化变量副本,并将其存入当前线程的ThreadLocalMap中,最终返回初始化后的值
java 复制代码
private T setInitialValue() {
    // 调用initialValue()获取默认值(可重写)
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

get()方法的核心细节

  • 初始化时机:只有当线程第一次调用get(),且Map不存在或无对应Key时,才会触发initialValue()初始化
  • 返回值:若Map中存在对应Value,返回该Value;否则返回initialValue()初始化后的值(默认null)
  • 线程隔离:不同线程调用get(),获取的是自己线程Map中的Value,互不干扰

4、remove():移除变量副本(避坑关键)

该方法用于移除当前线程中,当前ThreadLocal对应的变量副本,是避免ThreadLocal内存泄漏的核心方法,很多开发者会忽略它。

源码片段:

java 复制代码
public void remove() {
    // 1. 获取当前线程的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 2. 若Map存在,移除当前ThreadLocal对应的键值对
    if (m != null)
        m.remove(this);
}

remove()方法的核心意义

  • 释放内存:移除Map中的键值对,让Value对象失去强引用,便于GC(垃圾回收)回收,避免内存泄漏
  • 避免线程污染:在线程池场景中,线程会被复用,若不及时remove(),线程下次复用会读取到上一次存储的旧数据,导致数据错乱(即线程污染)

四、内存泄露问题

1、Entry类(存储键值对)

ThreadLocalMap的内部类Entry,用于存储<ThreadLocal, Value>键值对,其中Key采用弱引用存储。

java 复制代码
static class ThreadLocalMap {
    // Entry是ThreadLocalMap的内部类,存储键值对
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // Value是强引用
        Object value;
        
        // 构造方法:Key是ThreadLocal,采用弱引用存储;Value是强引用
        Entry(ThreadLocal<?> k, Object v) {
            super(k); // super()调用WeakReference的构造方法,将k设为弱引用
            value = v;
        }
    }
    
    // ThreadLocalMap的底层数组,存储Entry对象
    private Entry[] table;
    
    // 其他代码省略...
}

2、ThreadLocal 为什么会内存泄漏?

把结构拆开看

plaintext 复制代码
Thread(GC Root)
  ↓(强引用)
ThreadLocalMap
  ↓(强引用)
Entry[] table
  ↓(强引用)
Entry
  ├─ key: WeakReference<ThreadLocal>  // 弱引用
  └─ value: Object                   // 强引用!

只要线程还活着(比如线程池核心线程),这条链永远存在,value 永远不会被 GC

3、弱引用Key的设计原因

这里的弱引用(WeakReference)是理解ThreadLocal内存泄漏的关键。弱引用的特点是:当GC触发时,若一个对象只有弱引用指向它,那么该对象会被立即回收

ThreadLocalMap将ThreadLocal(Key)设为弱引用,目的是: 当ThreadLocal对象失去强引用(如开发者不再使用该ThreadLocal,将其赋值为null)时,GC可以回收该ThreadLocal对象(Key),靠弱引用兜底解决ThreadLocal对象本身的泄露(不remove()的情况下)。如果你把 ThreadLocal 定义为静态常量 + 主动 remove (),弱引用就没发挥直接作用。

但这里有一个隐患:Value是强引用,即使Key被GC回收,Value仍然会被Entry引用,而Entry又被ThreadLocalMap引用,ThreadLocalMap又被Thread引用。若Thread一直存活(如线程池中的核心线程),那么Value会一直无法被回收,最终导致内存泄漏(ThreadLocal 典型内存泄漏)。

这也是为什么必须调用remove()方法的原因------remove()会直接移除整个Entry,让Value失去强引用,从而被GC回收。

五、实战场景:ThreadLocal的正确使用方式

场景1:Web应用------用户上下文传递(最常用)

Web开发中,一个请求从Controller接收到底层DAO处理,往往需要经过多层调用(Controller→Service→DAO)。很多场景下,我们需要在整个请求生命周期中共享上下文信息(如当前登录用户、请求ID、租户ID等)。

若通过方法参数层层传递这些信息,会导致代码冗余、可读性差、扩展性差;使用ThreadLocal可以优雅解决这个问题,将上下文信息存入ThreadLocal,全链路可直接获取

1.1、定义用户上下文工具类(核心)

java 复制代码
import lombok.Data;

/**
 * 用户上下文工具类,用于存储当前请求线程的用户信息
 */
public class UserContextHolder {
    // 1. 定义private static ThreadLocal,存储用户上下文
    private static final ThreadLocal<UserContext> USER_CONTEXT_HOLDER = ThreadLocal.withInitial(UserContext::new);
    
    // 私有构造器,禁止实例化
    private UserContextHolder() {}
    
    // 2. 设置用户上下文
    public static void setUserContext(UserContext userContext) {
        USER_CONTEXT_HOLDER.set(userContext);
    }
    
    // 3. 获取用户上下文(全链路可调用)
    public static UserContext getUserContext() {
        return USER_CONTEXT_HOLDER.get();
    }
    
    // 4. 移除用户上下文(关键:请求结束后调用,避免内存泄漏和线程污染)
    public static void clear() {
        USER_CONTEXT_HOLDER.remove();
    }
    
    // 定义用户上下文实体类,存储需要共享的信息
    @Data
    public static class UserContext {
        private Long userId; // 用户ID
        private String username; // 用户名
        private String role; // 角色
        private String tenantId; // 租户ID
    }
}

1.2、拦截器中设置和清理上下文

在请求拦截器中,解析请求头中的Token,获取用户信息,存入ThreadLocal;请求结束后,调用clear()清理上下文(无论请求成功还是失败,都要清理)。

java 复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 登录拦截器,用于解析用户信息并设置到ThreadLocal中
 */
public class LoginInterceptor implements HandlerInterceptor {
    
    // 依赖注入用户服务,用于解析Token获取用户信息
    private final UserService userService;
    
    public LoginInterceptor(UserService userService) {
        this.userService = userService;
    }
    
    // 请求处理前:解析Token,设置用户上下文
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从请求头获取Token
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            response.setStatus(401);
            return false;
        }
        
        // 2. 解析Token,获取用户信息(实际开发中需做Token校验)
        UserContextHolder.UserContext userContext = userService.parseToken(token);
        
        // 3. 将用户上下文存入ThreadLocal
        UserContextHolder.setUserContext(userContext);
        
        return true;
    }
    
    // 请求处理后:清理用户上下文(无论成功还是失败)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserContextHolder.clear(); // 关键:避免内存泄漏和线程污染
    }
}

1.3、全链路使用上下文信息

Controller、Service、DAO层可直接调用UserContextHolder.getUserContext()获取用户信息,无需层层传递参数。

java 复制代码
// Controller层
@RestController
@RequestMapping("/order")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/create")
    public Result createOrder(@RequestBody OrderDTO orderDTO) {
        // 直接获取用户上下文
        UserContextHolder.UserContext userContext = UserContextHolder.getUserContext();
        System.out.println("当前登录用户:" + userContext.getUsername());
        
        orderService.createOrder(orderDTO);
        return Result.success();
    }
}

// Service层
@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderDAO orderDAO;
    
    @Override
    public void createOrder(OrderDTO orderDTO) {
        // 直接获取用户上下文,用于权限校验、数据隔离
        UserContextHolder.UserContext userContext = UserContextHolder.getUserContext();
        if (!"ADMIN".equals(userContext.getRole())) {
            throw new PermissionDeniedException("无下单权限");
        }
        
        // 处理下单逻辑
        OrderDO orderDO = OrderConvert.INSTANCE.dtoToDo(orderDTO);
        orderDO.setCreateBy(userContext.getUserId()); // 设置创建人ID
        orderDAO.insert(orderDO);
    }
}

场景2:数据库连接隔离(JDBC/MyBatis底层)

数据库连接池(如Druid、HikariCP)的底层实现中,大量使用了ThreadLocal来管理数据库连接(Connection)。核心目的是:保证一个线程在一次事务中,全程使用同一个Connection对象,避免多线程共用Connection导致的事务混乱。

核心原理

  • 线程发起数据库操作时,从连接池获取一个Connection对象,存入ThreadLocal
  • 该线程在本次事务中的所有数据库操作(如查询、修改),都从ThreadLocal中获取同一个Connection
  • 事务提交或回滚后,将Connection归还给连接池,并调用remove()清理ThreadLocal中的Connection
  • 这样既保证了事务的一致性,又避免了多线程共用Connection导致的并发问题

简化代码示例(模拟连接池底层)

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 模拟数据库连接池,基于ThreadLocal管理Connection
 */
public class ConnectionPool {
    
    // 数据库连接参数(实际开发中从配置文件读取)
    private static final String URL = "jdbc:mysql://localhost:3306/test";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";
    
    // ThreadLocal:存储当前线程的Connection,保证线程隔离
    private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();
    
    // 私有构造器,禁止实例化
    private ConnectionPool() {}
    
    // 1. 获取当前线程的Connection(若不存在,从连接池获取并存入ThreadLocal)
    public static Connection getConnection() throws SQLException {
        Connection connection = CONNECTION_HOLDER.get();
        if (connection == null || connection.isClosed()) {
            // 模拟从连接池获取连接(实际连接池会有连接复用逻辑)
            connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            // 将连接存入ThreadLocal
            CONNECTION_HOLDER.set(connection);
        }
        return connection;
    }
    
    // 2. 提交事务并释放连接
    public static void commit() throws SQLException {
        Connection connection = CONNECTION_HOLDER.get();
        if (connection != null && !connection.isClosed()) {
            connection.commit();
            // 释放连接(归还给连接池)
            connection.close();
            // 清理ThreadLocal中的连接
            CONNECTION_HOLDER.remove();
        }
    }
    
    // 3. 回滚事务并释放连接
    public static void rollback() throws SQLException {
        Connection connection = CONNECTION_HOLDER.get();
        if (connection != null && !connection.isClosed()) {
            connection.rollback();
            connection.close();
            CONNECTION_HOLDER.remove();
        }
    }
}

场景3:全链路日志追踪(TraceId传递)

分布式系统中,一个请求会经过多个服务、多个线程处理,排查问题时需要通过一个唯一的TraceId,将整个请求链路的日志串联起来。ThreadLocal可用于存储当前线程的TraceId,确保日志打印时能快速关联到同一个请求。

代码示例

java 复制代码
import java.util.UUID;

/**
 * 日志追踪工具类,基于ThreadLocal存储TraceId
 */
public class TraceIdUtil {
    
    // 存储当前线程的TraceId(唯一标识一个请求链路)
    private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();
    
    // 私有构造器,禁止实例化
    private TraceIdUtil() {}
    
    // 1. 生成TraceId并存入ThreadLocal(请求入口调用)
    public static void generateTraceId() {
        // 生成UUID作为TraceId(确保全局唯一)
        String traceId = UUID.randomUUID().toString().replace("-", "");
        TRACE_ID_HOLDER.set(traceId);
    }
    
    // 2. 获取当前线程的TraceId
    public static String getTraceId() {
        // 若TraceId不存在,生成一个默认的(避免空指针)
        return TRACE_ID_HOLDER.get() == null ? "DEFAULT_TRACE_ID" : TRACE_ID_HOLDER.get();
    }
    
    // 3. 清理TraceId(请求结束调用)
    public static void clearTraceId() {
        TRACE_ID_HOLDER.remove();
    }
    
    // 4. 打印日志(自动拼接TraceId)
    public static void log(String message) {
        String traceId = getTraceId();
        System.out.printf("[%s] [%s] %s%n", Thread.currentThread().getName(), traceId, message);
    }
}

// 使用示例
public class TraceDemo {
    public static void main(String[] args) {
        // 模拟请求入口:生成TraceId
        TraceIdUtil.generateTraceId();
        
        try {
            // 模拟链路调用,打印日志
            TraceIdUtil.log("请求开始处理");
            service1();
            service2();
            TraceIdUtil.log("请求处理完成");
        } finally {
            // 请求结束:清理TraceId
            TraceIdUtil.clearTraceId();
        }
    }
    
    private static void service1() {
        TraceIdUtil.log("service1:处理业务逻辑");
    }
    
    private static void service2() {
        TraceIdUtil.log("service2:处理业务逻辑");
    }
}

注意事项

  • TraceId生成时机:在请求入口(如网关、Controller拦截器)生成TraceId,确保整个链路的TraceId唯一
  • 跨服务传递:分布式系统中,TraceId需要通过请求头传递到其他服务,其他服务接收后存入自己的ThreadLocal
  • 清理时机:请求结束后必须清理TraceId,避免线程池复用导致的TraceId错乱
相关推荐
m0_607076607 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
青云计划8 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿8 小时前
Jsoniter(java版本)使用介绍
java·开发语言
NEXT068 小时前
二叉搜索树(BST)
前端·数据结构·面试
NEXT068 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
探路者继续奋斗8 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
夏鹏今天学习了吗9 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
消失的旧时光-19439 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
A懿轩A9 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
乐观勇敢坚强的老彭10 小时前
c++寒假营day03
java·开发语言·c++