Skip to content

ThreadLocal 最佳实践

ThreadLocal 的 API 就那么几个,但工程中的用法却有门道。

用得好,它是传递上下文的利器;用不好,它是内存泄漏和诡异 bug 的根源。

经典使用场景

场景一:用户上下文传递

最常见的场景:HTTP 请求中的用户信息,需要在 Controller、Service、DAO 多层之间传递。

java
public class UserContextHolder {

    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

    public static User getCurrentUser() {
        return currentUser.get();
    }

    public static void setUser(User user) {
        currentUser.set(user);
    }

    // ⚠️ 用完必须清理
    public static void clear() {
        currentUser.remove();
    }
}

Filter 中设置,拦截器中清理:

java
@WebFilter
public class UserContextFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        try {
            HttpServletRequest request = (HttpServletRequest) req;
            User user = extractUser(request);
            UserContextHolder.setUser(user);  // 设置
            chain.doFilter(req, res);
        } finally {
            UserContextHolder.clear();  // 清理
        }
    }
}

场景二:数据库连接(手动事务)

不使用 Spring 声明式事务时,手动管理连接的经典模式:

java
public class TransactionManager {

    private static final ThreadLocal<Connection> connection =
        ThreadLocal.withInitial(() -> {
            // 懒加载,从数据源获取连接
            return DataSource.getConnection();
        });

    public static Connection getConnection() {
        return connection.get();
    }

    public static void beginTransaction() throws SQLException {
        Connection conn = getConnection();
        conn.setAutoCommit(false);
    }

    public static void commit() throws SQLException {
        getConnection().commit();
        getConnection().setAutoCommit(true);
    }

    public static void rollback() throws SQLException {
        getConnection().rollback();
        getConnection().setAutoCommit(true);
    }

    public static void close() {
        connection.remove();
    }
}

// Service 层使用
public void transfer(String from, String to, double amount) {
    try {
        TransactionManager.beginTransaction();
        // 转账操作
        TransactionManager.commit();
    } catch (Exception e) {
        TransactionManager.rollback();
        throw e;
    } finally {
        TransactionManager.close();
    }
}

场景三:链路追踪(TraceId)

每个请求分配一个唯一 ID,贯穿整个调用链:

java
public class TraceContext {

    private static final ThreadLocal<String> traceId =
        ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

    private static final ThreadLocal<List<String>> logs =
        ThreadLocal.withInitial(ArrayList::new);

    public static String getTraceId() {
        return traceId.get();
    }

    public static void addLog(String log) {
        logs.get().add("[" + getTraceId() + "] " + log);
    }

    public static List<String> getLogs() {
        return new ArrayList<>(logs.get());
    }

    public static void clear() {
        traceId.remove();
        logs.remove();
    }
}

场景四:安全上下文

Spring Security 的 SecurityContextHolder 底层就是 ThreadLocal:

java
public class SecurityContextHolder {

    private static final ThreadLocal<SecurityContext> context =
        ThreadLocal.withInitial(SecurityContext::new);

    public static SecurityContext getContext() {
        return context.get();
    }

    public static void setContext(SecurityContext ctx) {
        context.set(ctx);
    }

    public static void clear() {
        context.remove();
    }

    public static void main(String[] args) {
        // 模拟认证
        SecurityContext ctx = getContext();
        ctx.setAuthenticated(true);
        ctx.setPrincipal("user1");
        setContext(ctx);

        // 任何层级都能获取
        checkPermission("READ");

        clear();
    }

    private static void checkPermission(String permission) {
        SecurityContext ctx = getContext();
        System.out.println("检查权限 " + permission + ": " +
            ctx.isAuthenticated());
    }

    static class SecurityContext {
        private boolean authenticated;
        private String principal;

        public boolean isAuthenticated() { return authenticated; }
        public void setAuthenticated(boolean b) { this.authenticated = b; }
        public String getPrincipal() { return principal; }
        public void setPrincipal(String p) { this.principal = p; }
    }
}

场景五:方法耗时追踪

无侵入地追踪每个方法的执行时间:

java
public class TimeTracker {

    private static final ThreadLocal<Long> startTime =
        ThreadLocal.withInitial(System::currentTimeMillis);

    public static void start() {
        startTime.set(System.currentTimeMillis());
    }

    public static long elapsed() {
        return System.currentTimeMillis() - startTime.get();
    }

    public static void clear() {
        startTime.remove();
    }

    // AOP 风格的使用
    public static <T> T execute(Supplier<T> action) {
        start();
        try {
            return action.get();
        } finally {
            System.out.println("耗时: " + elapsed() + "ms");
            clear();
        }
    }
}

必须清理的场景

1. 线程池

线程复用,必须每次任务结束后清理:

java
// ❌ 错误
executor.submit(() -> {
    context.set(value);
    // 任务结束,context 没有被清理
});

// ✅ 正确
executor.submit(() -> {
    try {
        context.set(value);
        // 业务逻辑
    } finally {
        context.remove();  // 必须清理
    }
});

2. 异步处理(CompletableFuture)

java
// ❌ 错误
CompletableFuture.supplyAsync(() -> {
    return context.get();  // 主线程的值拿不到
});

// ✅ 正确:手动传递
CompletableFuture.supplyAsync(() -> {
    String value = context.get();  // ITL 可以继承,ThreadLocal 拿不到
    return process(value);
});

3. 框架整合(Filter / Interceptor)

Web 框架中的标准做法:

java
// Filter(入口设置,出口清理)
public class ContextFilter {
    private static final ThreadLocal<Context> ctx = new ThreadLocal<>();

    public void doFilter(...) {
        try {
            ctx.set(buildContext());
            chain.doFilter();
        } finally {
            ctx.remove();
        }
    }
}

清理时机总览

场景清理时机
Web 请求Filter/Interceptor finally
线程池任务Runnable finally
定时任务Task finally
单线程使用try-finally

封装最佳实践

方式一:静态工具类(最常见)

java
public final class Context {
    private Context() {}

    private static final ThreadLocal<User> USER = new ThreadLocal<>();
    private static final ThreadLocal<Trace> TRACE = new ThreadLocal<>();

    public static User getUser() { return USER.get(); }
    public static void setUser(User u) { USER.set(u); }
    public static void clearUser() { USER.remove(); }

    public static Trace getTrace() { return TRACE.get(); }
    public static void setTrace(Trace t) { TRACE.set(t); }
    public static void clearTrace() { TRACE.remove(); }

    // 一键清理所有
    public static void clearAll() {
        USER.remove();
        TRACE.remove();
    }
}

方式二:AutoCloseable(更优雅)

java
public class ThreadLocalHolder<T> implements AutoCloseable {
    private final ThreadLocal<T> local = new ThreadLocal<>();

    public void set(T value) {
        local.set(value);
    }

    public T get() {
        return local.get();
    }

    @Override
    public void close() {
        local.remove();
    }
}

// 使用
try (ThreadLocalHolder<String> holder = new ThreadLocalHolder<>()) {
    holder.set("value");
    // 业务逻辑
} // 自动清理

方式三:模板方法封装

java
public class ContextTemplate {

    public static <T, R> R execute(Context ctx, Supplier<R> supplier) {
        ContextHolder.set(ctx);
        try {
            return supplier.get();
        } finally {
            ContextHolder.clear();
        }
    }

    public static <T> void execute(Context ctx, Runnable runnable) {
        ContextHolder.set(ctx);
        try {
            runnable.run();
        } finally {
            ContextHolder.clear();
        }
    }
}

常见错误汇总

错误后果解决
set 后不 remove内存泄漏try-finally
线程池任务不清理数据污染任务内 try-finally
子线程需要继承拿不到值InheritableThreadLocal / TTL
多级 ThreadLocal 只清理一个部分泄漏统一清理入口
在 finally 中 remove 后又 set白清理设计好执行顺序

选择口诀

单线程 → ThreadLocal
子线程要继承 → InheritableThreadLocal
线程池要继承 → TransmittableThreadLocal
Web 请求 → Filter 中统一清理

要点回顾

  • ThreadLocal 适合传递线程上下文,避免层层传参
  • 用完必须 remove(),try-finally 是标准做法
  • Web 应用在 Filter 中设置和清理
  • 线程池场景必须每次任务后清理
  • 封装成工具类,统一管理 set/get/remove

用好 ThreadLocal 的核心就一句话:入口 set,出口 remove,中间只 get。

相关链接

基于 VitePress 构建