ThreadLocal

发布于:2025-03-18 ⋅ 阅读:(22) ⋅ 点赞:(0)

多线程和 ThreadLocal 是 Java 并发编程中的两个重要概念,它们在处理线程安全和资源隔离时扮演关键角色。

1. 多线程基础

1.1 什么是多线程?

  • 线程:是操作系统能够调度的最小执行单元,一个进程可以包含多个线程。

  • 多线程:指程序中存在多个线程并发执行,以提高 CPU 利用率或实现异步任务处理。

  • 并发问题:当多个线程共享同一资源时,可能引发竞态条件(Race Condition)、数据不一致等问题。

1.2 线程的生命周期

线程有以下状态:

  • New:线程被创建但未启动。

  • Runnable:线程正在执行或等待 CPU 时间片。

  • Blocked/Waiting:线程因等待锁、I/O 或条件变量而暂停。

  • Terminated:线程执行完毕或异常终止。

1.3 线程同步机制

为了解决并发问题,常用的同步工具有:

  • synchronized:通过锁机制保证代码块或方法的原子性。

  • Lock:如 ReentrantLock,提供更灵活的锁控制。

  • volatile:保证变量的可见性(但不保证原子性)。

  • Atomic 类:如 AtomicInteger,通过 CAS(Compare-and-Swap)实现无锁线程安全。

2. ThreadLocal的引入

在多线程环境中,共享变量的线程安全问题通常需要通过同步机制解决。但某些场景下,我们希望每个线程拥有自己的变量副本,避免共享,从而彻底消除同步需求,例如:

  • 数据库连接管理(每个线程独立连接)。
  • 用户会话信息(每个请求对应一个线程)。

2.1 什么是ThreadLocal?

ThreadLocal 是 Java 中用于实现线程封闭的工具类,它能为每个线程创建一个独立的变量副本,不同线程之间互不干扰。简单来说,ThreadLocal 让每个线程都能拥有自己的“私有”数据。

假设有一个储物柜(ThreadLocal),每个线程(人)都可以往自己的储物柜里存东西,且只能访问自己的储物柜,不会拿错别人的东西。

2.2 ThreadLocal的核心机制

1. 线程隔离
  • 每个线程内部有一个 ThreadLocalMap(类似Map结构),用来存储该线程的所有 ThreadLocal 变量。
  • 不同线程的 ThreadLocalMap 彼此独立,互不可见。
2. 存储结构
  • 每个 ThreadLocal 对象通过 threadLocalHashCode 唯一标识自己。
  • ThreadLocalMap 的 key 是 ThreadLocal 对象,value 是线程存储的值。

2.3 ThreadLocal的使用方法

1. 创建ThreadLocal变量

private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

2. 操作数据

// 设置值(仅当前线程可见)
threadLocal.set("Hello");

// 获取值
String value = threadLocal.get();  // 输出 "Hello"

// 删除值(防止内存泄漏)
threadLocal.remove();

注意事项:使用完ThreadLocal后必须调用remove()清除数据,防止内存泄漏。

2.4 ThreadLocal的典型使用场景

1. 线程级上下文管理:

        在Web应用中存储用户登录信息(如用户ID),避免在方法参数中层层传递。

public class UserContext {
    private static ThreadLocal<Long> userHolder = new ThreadLocal<>();

    public static void setUserId(Long userId) {
        userHolder.set(userId);
    }

    public static Long getUserId() {
        return userHolder.get();
    }

    public static void clear() {
        userHolder.remove();
    }
}
2. 数据库连接管理:

在事务中,保证一个线程的所有数据库操作使用同一个 Connection。

public class ConnectionHolder {
    private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
        return dataSource.getConnection();  // 初始化连接
    });

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

    public static void close() {
        connectionHolder.get().close();
        connectionHolder.remove();
    }
}
3. 日期格式化:

SimpleDateFormat 是线程不安全的,可用 ThreadLocal 为每个线程创建独立实例。

private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd")
);

3. ThreadLocal实现权限验证

使用 ThreadLocal 实现权限验证是一种常见的实践,尤其在 Web 应用中,比如用户登录后,后续的请求需要携带token,并在服务端验证权限。通过将用户身份信息(如用户ID、角色、权限列表)存储在 ThreadLocal 中,可以避免在方法参数中层层传递用户上下文,同时保证线程安全。可以在Spring Security或者自定义的拦截器中,获取用户信息后存入ThreadLocal,后续业务方法可以直接使用。

3.1 实现思路

  1. 拦截请求:在请求进入时(如通过过滤器或拦截器),解析Token或Cookie获取用户身份信息。
  2. 存储用户信息:将用户信息存入ThreadLocal,后续业务代码可直接从ThreadLocal中获取。
  3. 权限验证:在需要权限控制的地方(如Service层或自定义注解),从ThreadLocal取出用户信息,验证权限。
  4. 清理数据:请求结束时(如拦截器后置处理),清理ThreadLocal,防止内存泄漏。

3.2 代码实现

定义ThreadLocal上下文工具类:

public class UserContext {
    // 存储用户信息的 ThreadLocal(使用静态内部类初始化,避免内存泄漏)
    private static final ThreadLocal<LoginUser> userHolder = new ThreadLocal<>();

    // 设置当前用户
    public static void setUser(LoginUser user) {
        userHolder.set(user);
    }

    // 获取当前用户
    public static LoginUser getUser() {
        return userHolder.get();
    }

    // 清理用户信息(必须调用!)
    public static void clear() {
        userHolder.remove();
    }

    // 用户信息封装类(示例)
    public static class LoginUser {
        private Long userId;
        private String username;
        private List<String> roles;  // 角色列表(如 "admin", "user")
        private List<String> permissions;  // 权限列表(如 "order:create", "user:delete")

        // 省略构造方法、getter/setter
    }
}

拦截器中解析用户信息并存入ThreadLocal

public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从请求头中获取 Token(示例)
        String token = request.getHeader("Authorization");
        
        // 2. 解析 Token 获取用户信息(实际需调用认证服务或查询数据库)
        LoginUser user = parseToken(token);
        
        // 3. 存入 ThreadLocal
        UserContext.setUser(user);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求结束后清理 ThreadLocal
        UserContext.clear();
    }

    // 模拟 Token 解析逻辑
    private LoginUser parseToken(String token) {
        // 实际需验证 Token 合法性,并查询用户权限(可结合 JWT 或 Redis)
        LoginUser user = new LoginUser();
        user.setUserId(1001L);
        user.setUsername("admin");
        user.setRoles(Arrays.asList("admin"));
        user.setPermissions(Arrays.asList("user:delete", "order:*"));
        return user;
    }
}

注册拦截器(Spring Boot配置)

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/**")  // 拦截所有请求
                .excludePathPatterns("/login");  // 排除登录接口
    }
}

在业务层进行权限验证

@Service
public class OrderService {
    public void deleteOrder(Long orderId) {
        // 从 ThreadLocal 获取当前用户
        LoginUser user = UserContext.getUser();
        
        // 1. 验证是否登录
        if (user == null) {
            throw new RuntimeException("未登录!");
        }
        
        // 2. 验证权限(示例:需要 "order:delete" 权限)
        if (!user.getPermissions().contains("order:delete")) {
            throw new RuntimeException("权限不足!");
        }
        
        // 3. 执行业务逻辑
        // ...
    }
}

3.3 注意事项

1. 内存泄漏:

        必须确保在请求结束时调用 UserContext.clear()(通常在拦截器的 afterCompletion 中清理)。

        如果使用线程池(如异步任务),线程会被复用,必须显式清理 ThreadLocal。

2. 分布式系统:

        ThreadLocal 只适用于单机环境。若服务是分布式的,用户信息应存储在 Redis 等共享存储中,通过 Token 解析获取。

3. 安全性:

        Token 解析逻辑需要严格防止伪造(如使用 JWT 签名或查询 Redis 校验有效性)。

4. 性能:

        频繁调用 ThreadLocal.get() 对性能影响极小,但权限列表不宜过大(建议控制在百级以内)。

3.4 适用场景

  • 单体架构需要Web应用(如Spring Boot后台管理系统)。
  • 需要减少方法参数传递的上下文信息(如用户身份)。
  • 配合AOP实现声明式权限控制。

4. 常见问题

4.1 ThreadLocal和synchronized的区别

  • ThreadLocal 通过空间换时间,每个线程独立操作数据,无需竞争锁。
  • synchronized 通过时间换空间,让线程排队访问共享数据。

4.2 子线程如何继承父线程的ThreadLocal数据

  • 使用 InheritableThreadLocal,但要注意线程池中复用线程时可能失效。

网站公告

今日签到

点亮在社区的每一天
去签到