文章目录
使用场景
在我们日常 Java Web 开发中难免遇到需要把一个参数层层的传递到最内层。
例如,用户进行操作需要在拦截器中从redis等缓存中间件去获取用户信息并判断是否过期,如果接下来的的业务方法需要用到用户信息时怎么获取呢?
Java的Web项目大部分都是基于Tomcat,每次访问都是一个新的线程,这样让我们联想到了ThreadLocal,每一个线程都独享一个ThreadLocal,在接收请求的时候set特定内容,在需要的时候get这个值。
先附上本文中demo源码演示地址,有兴趣的可以看下
ThreadLocal
Demo
模拟一个普通的用户请求(新启动一个线程)
- 请求先经过拦截器,拦截器中必然需要获取用户信息,同时调用ThreadLocal.set(userInfo)将用户信息塞入线程上下文中
- 进行业务处理(业务处理时从ThreadLocal中获取用户信息,避免参数层层传递)
ThreadLocal封装类
public class ThreadLocalHolder {
/**
* 普通THREAD_LOCAL
*/
private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();
public static UserInfo getUser() {
return THREAD_LOCAL.get();
}
public static void setUser(UserInfo userInfo) {
THREAD_LOCAL.set(userInfo);
}
}
测试类,启动一个线程模拟一个普通的Web同步请求
public static void main(String[] args) {
BusinessService businessService = new BusinessService();
LoginInterceptor loginInterceptor = new LoginInterceptor();
//模拟一个普通的同步web请求
new Thread(() -> {
// 模拟用户身份拦截器
loginInterceptor.userInterceptor();
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalHolder.getUser());
// 拦截器通过后 同步处理业务
businessService.doBusiness();
}).start();
}
模拟Web项目中的拦截器实现,从缓存中获取用户信息,塞入ThreadLocal中
public class LoginInterceptor {
/**
* 模拟拦截方法
*/
public void userInterceptor() {
UserInfo userInfo = getUserFromRedis();
//将用户信息塞入ThreadLocal中
ThreadLocalHolder.setUser(userInfo);
}
/**
* 模拟从redis中获取信息,这里写死直接返回
*
* @return
*/
public UserInfo getUserFromRedis() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1L);
userInfo.setUserName("chenyin");
return userInfo;
}
}
业务处理类,获取用户信息,再处理业务
public class BusinessService {
/**
* 模拟同步处理业务
*/
public void doBusiness() {
//获取用户信息,避免显示参数传递
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalHolder.getUser());
//业务处理。。略去
}
/**
* 模拟异步处理业务
*/
public void doBusinessAsync() {
new Thread(() -> {
//获取用户信息,避免显示参数传递
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalHolder.getUser());
//业务处理。。略去
}).start();
}
}
main方法执行结果如下
可以看到,同一个线程中,即无论调用层级多深,也不需要将UserInfo作为参数层层传递,直接调用ThreadLocal.get()
方法即可获取用户信息
ThreadLocal存储结构
首先提出一个问题,ThreadLocal中set()方法设的值具体存储在哪里?
先看几个关键的变量定义
Thread类中变量定义
public class Thread implements Runnable {
//略去其他变量定义
//普通线程上下文存储所在Map
ThreadLocal.ThreadLocalMap threadLocals = null;
//InheritableThreadLocal可继承线程本地变量存储所在Map
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal.ThreadLocalMap中变量定义,Entry中的Key(ThreadLocal类型)是个WeakReference弱引用
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
}
再看下set方法实现
public void set(T value) {
//获取当前线程对象t
Thread t = Thread.currentThread();
//获取t中的 ThreadLocal.ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
//往ThreadLocalMap中的tables中加入数据,key为当前ThreadLocal对象,value为用户传入的值
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal之所以能做不到不同线程之间的隔离性,就是因为set方法设的值不是存在我们定义的ThreadLocal变量中,而是存储在每个线程的变量(ThreadLocal.ThreadLocalMap)中
再提出一个问题,ThreadLocalMap.Entry中的key值为什么是ThreadLocal类型?
假设有如下场景,同一个线程中同时使用了2个ThreadLocal
public static void main(String[] args) {
ThreadLocal<Integer> threadLocalA = new ThreadLocal<>();
ThreadLocal<Integer> threadLocalB = new ThreadLocal<>();
threadLocalA.set(1);
threadLocalB.set(2);
}
同一线程中可能定义了不同的ThreadLocal变量,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的,因此Key为ThreadLocal能够根据ThreadLocal中的hashCode唯一确定其value在table中的下标
关键API
//从线程上下文中获取值
public T get() ;
//将值设入线程上下文中,供同一线程后续使用
public void set(T value) ;
//清除线程上下文
public void remove() ;
set方法实现
源码如下
public void set(T value) {