ThreadLocal 实战应用

1 什么是 ThreadLocal?

ThreadLocal 是一个关于创建线程局部变量的类。

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程则无法访问和修改。ThreadLocal 在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。

2 有什么作用?
2.1 set once,get everywhere

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在 Session 或者 Token 中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿 Session 来说,我们要在接口参数中加上 HttpServletRequest 对象,然后调用 getSession 方法,且每一个需要用户信息的接口都要加上这个参数,才能获取 Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用 ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入 ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用 ThreadLocal 的 get()方法 (异步程序中 ThreadLocal 是不可靠的)

2.2 线程安全,空间换时间

在 Spring 的 Web 项目中,我们通常会将业务分为 Controller 层,Service 层,Dao 层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于 Dao 层使用单例,那么负责数据库连接的 Connection 也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring 是如何解决这个问题的呢?

在 Spring 项目中 Dao 层中装配的 Connection 肯定是线程安全的,其解决方案就是采用 ThreadLocal 方法,当每个请求线程使用 Connection 的时候, 都会从 ThreadLocal 获取一次,如果为 null,说明没有进行过数据库连接,连接后存入 ThreadLocal 中,如此一来,每一个请求线程都保存有一份 自己的 Connection。于是便解决了线程安全问题

3 ThreadLocal 实战应用

3.1 ehr 中的使用

在登录拦截器中将用户信息写入,后续使用时方便取值

package com.cloud.api.interceptor;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.cloud.bean.User;
import com.cloud.common.context.ApiContextHolder;
import com.cloud.common.context.ApiResponseUtil;
import com.cloud.common.redis.RedisPre;
import com.cloud.common.result.CommonResult;
import com.cloud.common.util.JwtUtil;
import com.cloud.common.vo.AuthVo;
import com.cloud.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class AuthInterceptor implements HandlerInterceptor {

    public static Map<String, User> USER_ACCOUNT = new HashMap<>();

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IUserService iUserService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");// 从 http 请求头中取出 token
        // 如果为空,则返回未登录
        if (StrUtil.isEmpty(token)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("token为空"));
            return false;
        }
        if (token.contains("token")){
            token = token.substring(6);
        }
        ApiContextHolder.setToken(token);
        AuthVo authVo = null;
        try {
            authVo = JwtUtil.getToken(token);
        }
        catch (JWTDecodeException ex){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("JWT解析错误"));
            return false;
        }
        if (ObjectUtil.isNull(authVo)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("JWT解析为空"));
            return false;
        }
        User user = USER_ACCOUNT.get(authVo.getUserAccount());
        if(USER_ACCOUNT.isEmpty()){
            USER_ACCOUNT = iUserService.getAccountMap();
            user = USER_ACCOUNT.get(authVo.getUserAccount());
            if(Objects.isNull(user)){
                USER_ACCOUNT = iUserService.getAccountMap();
                user = USER_ACCOUNT.get(authVo.getUserAccount());
            }
        }
        if (ObjectUtil.isNull(user)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("用户不存在"));
            return false;
        }
        if(!JwtUtil.verifierToken(token)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.unauthorized("验证失败"));
            return false;
        }
        // 检查该用户是否被T出
        String tokenCache = stringRedisTemplate.opsForValue().get(RedisPre.TOKEN_PRE + authVo.getUserAccount());
        if (StrUtil.isEmpty(tokenCache)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.signal());
            return false;
        }
        if (!token.equals(tokenCache)){
            ApiContextHolder.clearAuthVo();
            ApiResponseUtil.sendJsonMessage(response, CommonResult.signal());
            return false;
        }
        // 存入CONTEXT信息入缓存
        ApiContextHolder.setAuthVo(authVo);
        return true;
    }
}
package com.cloud.common.context;

import com.cloud.common.vo.AuthVo;

public class ApiContextHolder {
    private static final ThreadLocal<AuthVo> CONTEXT = new ThreadLocal<>();

    private static final ThreadLocal<String> CONTEXT_TOKEN = new ThreadLocal<>();

    // AuthVo
    public static void setAuthVo(AuthVo authVo){
        CONTEXT.set(authVo);
    }

    public static AuthVo getAuthVo(){
        return CONTEXT.get();
    }

    public static void clearAuthVo(){
        CONTEXT.remove();
    }

    // TOKEN
    public static void setToken(String token){
        CONTEXT_TOKEN.set(token);
    }

    public static String getToken(){
        return CONTEXT_TOKEN.get();
    }
}

在使用 ThreadLocal 类型变量进行相关操作时,都会通过当前线程获取到 ThreadLocalMap 来完成操作。每个线程的 ThreadLocalMap 是属于线程自己的,ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了 ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。

不同账号用户登录,同一个线程通过ThreadLocal获取的用户信息都是各自自己的信息,相互隔离。

参考:ThreadLocal源码解析及实战应用_Java_京东科技开发者_InfoQ写作社区

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stone.小小的太阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值