Skip to main content

1.ThreadLocal存储用户信息

ThreadLocal简介

ThreadLocal 是 Java 中一个非常有用的工具,尤其在多线程环境下,使用它可以为每个线程独立存储变量,而不会与其他线程共享。

这在存储用户信息时非常方便,比如在 web 应用程序中,每个请求可能对应一个独立的线程,使用 ThreadLocal 可以在每个请求线程中存储和获取用户信息。

ThreadLocal 最常见的应用场景是需要隔离线程间的数据共享,确保每个线程拥有自己独立的变量副本。它适用于线程生命周期较长,且需要在多个方法或组件间共享特定数据的场景,但需小心管理,以避免潜在的内存泄漏问题。

示例1:如何使用 ThreadLocal 存储和获取用户信息

public class UserContext {
// 创建 ThreadLocal 变量,用于存储每个线程独立的用户信息
private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

// 设置当前线程的用户信息
public static void setUser(User user) {
userThreadLocal.set(user);
}

// 获取当前线程的用户信息
public static User getUser() {
return userThreadLocal.get();
}

// 清除当前线程的用户信息(防止内存泄漏)
public static void clear() {
userThreadLocal.remove();
}
}

// 假设有一个 User 类表示用户信息
class User {
private String username;
private String email;

public User(String username, String email) {
this.username = username;
this.email = email;
}

public String getUsername() {
return username;
}

public String getEmail() {
return email;
}
}

UserContext 是一个自定义的类,用于管理和存储与用户相关的信息。

在上面的示例中,UserContext 封装了对 ThreadLocal 变量的管理,以便为每个线程单独存储和访问用户信息。它提供了 setUsergetUserclear 三个静态方法:

  1. setUser(User user) :将用户信息存储到当前线程的 ThreadLocal 中。
  2. getUser() :从当前线程的 ThreadLocal 中获取用户信息。
  3. clear() :从当前线程的 ThreadLocal 中移除用户信息,防止内存泄漏。

在业务逻辑中,可以通过以下方式设置、获取和清除用户信息:

public class UserService {

public void processRequest(String username, String email) {
// 模拟设置用户信息
User user = new User(username, email);
UserContext.setUser(user);

// 获取用户信息
User currentUser = UserContext.getUser();
System.out.println("Current User: " + currentUser.getUsername());

// 处理完毕后清除用户信息
UserContext.clear();
}
}

在 Web 应用中,可以在请求进入时设置用户信息(例如在过滤器或拦截器中),并在请求完成后进行清理。

示例2:ThreadLocal结合拦截器使用

可以使用拦截器(Interceptor)来结合 UserContext 实现用户信息的自动注入和清理。

  1. 创建 UserContext

    使用 UserContext 将用户信息与当前线程关联。

    public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

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

    public static User getUser() {
    return userThreadLocal.get();
    }

    public static void clear() {
    userThreadLocal.remove();
    }
    }
  2. 创建 UserInterceptor 拦截器

    在拦截器中,可以在请求处理前设置用户信息,处理完后进行清理。

    preHandle 方法中,拦截器从请求头中提取用户信息并存储到 UserContext 中。

    afterCompletion 方法则在请求完成后清理 ThreadLocal,以避免内存泄漏。

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.springframework.web.servlet.HandlerInterceptor;

    public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 假设从请求头中获取用户名和邮箱
    String username = request.getHeader("username");
    String email = request.getHeader("email");

    if (username != null && email != null) {
    // 创建用户对象并设置到 UserContext
    User user = new User(username, email);
    UserContext.setUser(user);
    }

    return true; // 返回 true 以继续处理请求
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // 请求处理完成后,清理 ThreadLocal 中的用户信息
    UserContext.clear();
    }
    }
  3. 注册拦截器

    在 Spring 的配置类中注册 UserInterceptor,以使其生效。

    UserInterceptor 会在每次请求进入控制器之前设置用户信息,并在请求完成后清理。

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    @Configuration
    public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new UserInterceptor()).addPathPatterns("/**");
    }
    }
  4. 在业务逻辑中获取用户信息

    通过 UserContext.getUser() 获取当前请求的用户信息。每个请求的用户信息都是线程安全的,不会被其他请求影响。

    public class UserService {

    public void process() {
    User currentUser = UserContext.getUser();
    if (currentUser != null) {
    System.out.println("Processing for user: " + currentUser.getUsername());
    } else {
    System.out.println("No user information available.");
    }
    }
    }

其他应用场景

数据库连接、会话管理

在应用中,每个线程处理一个请求,且通常需要独立的数据库连接。ThreadLocal 可以为每个线程单独存储数据库连接对象或会话对象,这样同一线程可以方便地多次访问同一个连接,而不同线程之间不会相互干扰。

public class DatabaseConnectionManager {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = createNewConnection(); // 创建新的数据库连接
connectionHolder.set(conn);
}
return conn;
}

public static void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
conn.close(); // 关闭连接
connectionHolder.remove(); // 清理 ThreadLocal
}
}
}

日志跟踪(如请求 ID 或追踪 ID)

在分布式系统中,常用 ThreadLocal 为每个请求分配一个唯一的跟踪 ID(Trace ID),用于日志跟踪。这样可以确保整个请求链路的日志都带有相同的追踪 ID,便于问题追踪和调试。

public class TraceContext {
private static final ThreadLocal<String> traceId = new ThreadLocal<>();

public static void setTraceId(String id) {
traceId.set(id);
}

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

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

格式化工具(如日期格式化)

日期格式化工具 SimpleDateFormat 是线程不安全的。如果多个线程同时使用同一个实例,可能会引发线程安全问题。通过 ThreadLocal 为每个线程创建独立的 SimpleDateFormat 实例,可以避免这个问题。

public class DateFormatter {
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public static String formatDate(Date date) {
return dateFormatThreadLocal.get().format(date);
}
}

使用注意

threadlocal可能会导致内存泄漏

在一些高并发的 Web 应用中可能会出现该问题。

这种内存泄漏的主要原因是 ThreadLocal 的生命周期绑定到线程上,而线程通常是被线程池复用的。这意味着,如果不及时清理 ThreadLocal 中的变量,那么当线程被重用时,ThreadLocal 中的旧数据仍然存在,导致无法被垃圾回收,从而可能引发内存泄漏。

内存泄漏的原因

ThreadLocal 的实现是通过每个线程维护一个 ThreadLocalMap 来存储数据,这个 Map 的 key 是 ThreadLocal 实例,value 是存储的实际对象:

  1. 线程复用 :在 Web 容器(如 Tomcat)中,线程池会复用线程。如果 ThreadLocal 的值没有清理,当线程被再次使用时,之前的值仍然保留,导致数据堆积。
  2. 弱引用ThreadLocalMap 中的 key(即 ThreadLocal 对象)是弱引用。如果 ThreadLocal 没有被外部强引用,它会被垃圾回收。但此时 ThreadLocalMap 中的 value(存储的用户信息等)仍然存在,导致无法被回收。

如何避免内存泄漏

可以采取以下措施避免 ThreadLocal 导致的内存泄漏:

  1. 手动清理 :在使用 ThreadLocal 后,调用 remove() 方法清除数据,确保线程池重用线程时不再持有之前的值。这通常在 finally 块中执行,以确保清理始终执行。例如:

    try {
    UserContext.setUser(user);
    // 执行与用户相关的操作
    } finally {
    UserContext.clear(); // 确保清理 `ThreadLocal` 值
    }
  2. 在拦截器的 afterCompletion 中清理 :如果是在 Web 应用中,可以在请求完成后(如 Spring MVC 的 afterCompletion 方法)清理 ThreadLocal。在上面的示例中,我们在 afterCompletion 方法中调用了 UserContext.clear() 进行清理。

  3. 限制使用范围 :尽量缩小 ThreadLocal 的作用范围,只在需要时使用,而不是在整个应用中随意使用,减少误用的可能性。

https://blog.csdn.net/qq_41432730/article/details/123156917

https://developer.aliyun.com/article/979821