Java面试系列文章
面试必知必会(8):CountDownLatch、CyclicBarrier、Semaphore、Exchanger
目录
- [一、什么是 ThreadLocal?](#一、什么是 ThreadLocal?)
- 二、底层原理:ThreadLocal是如何实现线程隔离的?
- [三、源码拆解:深入ThreadLocal的核心方法(JDK 8)](#三、源码拆解:深入ThreadLocal的核心方法(JDK 8))
-
- 1、initialValue():初始化变量副本
- [2、set(T value):存储变量副本](#2、set(T value):存储变量副本)
-
- [辅助方法1:getMap(Thread t)](#辅助方法1:getMap(Thread t))
- [辅助方法2:createMap(Thread t, T firstValue)](#辅助方法2:createMap(Thread t, T firstValue))
- set()方法的核心细节
- 3、get():获取变量副本
- 4、remove():移除变量副本(避坑关键)
- 四、内存泄露问题
-
- 1、Entry类(存储键值对)
- [2、ThreadLocal 为什么会内存泄漏?](#2、ThreadLocal 为什么会内存泄漏?)
- 3、弱引用Key的设计原因
- 五、实战场景: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的底层隔离机制,依赖于「Thread、ThreadLocal、ThreadLocalMap」三个核心对象的配合,三者的关系是理解原理的关键:
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错乱