ThrealLocal的通俗含义:将数据存到线程中的本地变量中(数据与线程绑定),且通过静态方法拿到当前线程从而拿到本地变量,然后拿到数据,实现了线程隔离;相当于每个线程拥有自己独立的数据空间,可以在里面存取数据,利用这个机制可以实现跨组件的通信,单个线程内的数据共享(不是多线程数据共享)
中间是本地变量,每个线程访问这个变量都会仅仅访问到自己线程内的ThreadLocalMap
ThrealLocal的应用简述:比如一次请求中,数据可以存在这个线程区域里,然后在任意地方把他拿出来(常见的用法,比如shiro利用静态方法拿到了绑定好用户信息的数据)
Ps:注意线程中存数据,是利用本地变量ThreadLocal做连接的,所以如果声明了不同的本地变量,就可以存不同类型的数据;
ThrealLocal的基本使用:
/**
* 声明一个本地变量,一般来讲不同的ThreadLocal对象,绑定的数据的意义不同
* 泛型是为了取出的时候不用强装
* 一般来讲是静态的,这样调用方便
*/
private static final ThreadLocal<UserEntity> USER_INFO = new ThreadLocal<>();
/**
* 基本API使用
*/
public static void main(String[] args) {
USER_INFO.set(new UserEntity(){{//线程绑定数据
setSex("M");
setUserName("GodSchool");
}});
USER_INFO.get();//取出和线程绑定的数据
USER_INFO.remove();//删除和线程绑定的数据
}
Ps:线程隔离的概念:只要线程绑定了数据,无论代码运行中哪个地方拿出来的都是自己线程的数据,线程隔离
ThrealLocal的探索起源:记得当初学习ThreadLocal的时候,仅仅只知道这个静态类可以实现线程安全,但对实际应用完全不知道什么场景下会用到这个技术;后来在一次偶然的调试中,在业务开发中我们经常遇到利用shiro的工具类拿到自己的session,然后拿到用户信息;我产生了一个疑问,session不是应该利用sessionId才能拿出自己的session吗,我没有传sessionId进去,这是知道拿到的session一定是自己的session呢?
后来我进入shiro的源码一看,最后看到了这个会话session是通过ThreadLocal拿出来的,这一样以来我就完全了解了ThreadLocal的作用了,首先shiro会有一个拦截器取出当前request的sessionId,然后利用这个sessionId拿到原本存在内存中的session(再在了MemorySessionDAO里面有个map,用sessionId作为key取出),然后再将这个session,利用ThreadLocal的set方法将这个session跟线程绑定,意味着在线程中调用ThreadLocal的get方法就可以拿出这个session,很显然ThreadLocal的方法是静态的,他能首先能识别当前线程是哪个线程,然后再在线程的对应区域中创建数据也可以从对应区域中取出数据,这就是ThreadLocal的作用,当然这只是通俗的描述,如果想了解ThreadLocal详细的原理和标准分析可以看其他博客,本文只根据应用场景做展示,原理只是略过;
ThrealLocal的应用模仿:
依据这个思想,写一个拦截器取出request的数据,然后根据标识id取出session,我们也可以这样做,我的这个案例是来源于我们是springcloud微服务架构,所以业务层没有引入shiro,我想实现在业务层调用一个静态方法可以拿到用户数据;
【1】先封装两个个实体类
ContextProvider(用于组织数据——————将用户信息,用户的request实体,response实体用一个对象统一存起来)
@Getter
class ContextProvider {
private HttpServletRequest request;
private HttpServletResponse response;
private UserEntity userEntity;
ContextProvider(HttpServletRequest request, HttpServletResponse response,UserEntity userEntity) {
this.request = request;
this.response = response;
this.userEntity=userEntity;
}
}
LocalProvider (用于封装操作————封装了本地变量的操作方法)
public class LocalProvider {
/**
* @原生的ThreadLocal
* 泛型可以标记数据类型,取出的时候就不用强转了
*/
private static final ThreadLocal<ContextProvider> USER_INFO = new ThreadLocal<>();
/**
* @UserInfo相关方法
*/
public static UserEntity getUserInfo() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
if (contextProvider == null) {
return null;
}
return contextProvider.getUserEntity();
}
/**
* @生命周期相关方法
*/
public static void init(HttpServletRequest request, HttpServletResponse response, UserEntity userEntity) {
USER_INFO.set(new ContextProvider(request, response, userEntity));
}
public static void destroy() {
USER_INFO.remove();
}
/**
* @Servlet相关方法
*/
public static HttpServletRequest getRequest() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
return contextProvider.getRequest();
}
public static HttpServletResponse getResponse() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
return contextProvider.getResponse();
}
}
【2】写一个拦截器(应用关键)
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
public void addInterceptors(InterceptorRegistry registry) {
/**
* @封装会话数据到ThreadLocal
*/
registry.addInterceptor(new HandlerInterceptor() {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token=request.getHeader("sessionId");
UserEntity userInfo=JwtUtil.parseToken(token);
LocalProvider.init(request,response,userInfo);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
/**
* @每次用完就清空
*/
LocalProvider.destroy();
}
}).addPathPatterns("/**")
.excludePathPatterns("/admin/user/queryUser/**");
}
}
【3】取出数据
依据我们的想法,数据已经和线程绑定存在某个区域,接下来我们就可以用静态方法取出来;
public IPage<InvoiceReimbVo> queryInvoice(InvoicePo po) {
UserEntity userInfo = LocalProvider.getUserInfo();
}
以上代码就是对应我们写的LocalProvider的这个方法,实际就是替代我们操作ThreadLocal的外包装饰,因为这样子写代码就更明确一点,java原生的东西实现的更细更底层更抽象,但是应用上我们可以根据场景将他语义化更明确;
public static UserEntity getUserInfo() {
ContextProvider contextProvider = (ContextProvider) USER_INFO.get();
if (contextProvider == null) {
return null;
}
return contextProvider.getUserEntity();
}
ThrealLocal的其他应用(跨组件通信)
对于框架来讲,当你的封装越来越多的时候,显然你想要传一个数据到某一个对象里,这是很难的,难道逐层传参,去研究框架的源码?这显然非常费力而且会造成不可预料的东西,那么ThreadLocal就派上用场了,当然这是某些场景下,比如我的shiro中因为想改造让shiro的sessionId由我生成的Token替代,且这个Token是经过jwt对实体类数据加密后生成的,我想把这个id放到shiro的框架里面并取出来作为id,这就很难实现了,利用ThreadLocal实现就非常简单,因为能保证jwt生成后再去执行session创建工作
【1】因此先把sessionId放到ThreadLocal中
【在登陆验证成功后执行的回调】
ShiroUtil.createTokenOnThread(user);
public class ShiroUtil {
……
private static final ThreadLocal<String> TOKEN = new ThreadLocal<>();
public static void createTokenOnThread(UserEntity userEntity){
TOKEN.set(JwtUtil.createJWT(userEntity));
}
public static String getTokenOnThread(){
return TOKEN.get();
}
}
【2】因此把sessionId从ThreadLocal中拿出
【这是重写了sessionDao,当第一次创建session后会进入这里】
protected Serializable doCreate(Session session) {
String token=ShiroUtil.getTokenOnThread();
assignSessionId(session, token);
storeSession(token, session);
return token;
}
那么我们就利用这个本地变量线程共享的机制实现了跨组件通信,不用再一步步传参去看框架源码,后续如果有时间我会给大家逐一分享Shiro的高级应用,大家可以关注我
ThrealLocal的简单源码
【1】set部分
很明显就是利用静态方法拿出当前线程
public void set(T value) {
Thread t = Thread.currentThread();//原来这就是线程隔离的根本原因
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//将当前本地变量作为key引用拿出value,一个线程可能有多个本地变量
else
createMap(t, value);//如果是空在创建一个map
}
然后利用当前线程拿到所有本地变量的map
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
【2】get部分
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//本地变量作为key拿出来
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
【3】ThreadLocalMap数据结构
显然是就是一个hashMap一样的东西
image.png
GodSchool
致力于简洁的知识工程,输出高质量的知识产出,我们一起努力
网友评论