赞
踩
点击上方“Java基基”,选择“设为星标”
做积极的人,而不是积极废人!
每天 14:00 更新文章,每天掉亿点点头发...
源码精品专栏
本文基于ThreadLocal
原理,实现了一个上下文状态管理组件Scope
,通过开启一个自定义的Scope
,在Scope
范围内,可以通过Scope
各个方法读写数据;
通过自定义线程池实现上下文状态数据的线程间传递;
提出了一种基于Filter
和Scope
的Request
粒度的上下文管理方案。
ThreadLocal
主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争,那ThreadLocal
是如何实现这一特性的呢?
从上图可知:
每个Thread
对象中都包含一个ThreadLocal.ThreadLocalMap
类型的threadlocals
成员变量;
该map对应的每个元素Entry
对象中:key是ThreadLocal
对象的弱引用,value是该threadlocal
变量在当前线程中的对应的变量实体;
当某一线程执行获取该ThreadLocal
对象对应的变量时,首先从当前线程对象中获取对应的threadlocals
哈希表,再以该ThreadLocal
对象为key查询哈希表中对应的value;
由于每个线程独占一个threadlocals
哈希表,因此线程间ThreadLocal
对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。
有人可能会问:ThreadLocalMap
是Thread
成员变量(非public,只有包访问权限,Thread和Threadlocal都在java.lang 包下,Thread可以访问ThreadLocal.ThreadLocalMap)
,定义却在ThreadLocal中,为什么要这么设计?
源码的注释给出了解释:ThreadLocalMap
就是维护线程本地变量设计的,就是让使用者知道ThreadLocalMap
就只做保存线程局部变量这一件事。
- public void set(T value) {
- Thread t = Thread.currentThread(); //获取当前线程
- ThreadLocalMap map = getMap(t); //从当前线程对象中获取threadlocals,该map保存了所用的变量实例
- if (map != null) {
- map.set(this, value);
- } else {
- createMap(t, value); //初始threadlocals,并设置当前变量
- }
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t); //从当前线程对象中获取threadlocals,该map保存了所用的变量实体
- if (map != null) {
- // 获取当前threadlocal对象对应的变量实体
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- // 如果map没有初始化,那么在这里初始化一下
- return setInitialValue();
- }
由于通过 ThreadLocal
的 set()
设置的值,只会设置当前线程对应变量实体,无法实现统一初始化所有线程的ThreadLocal
的值。ThreadLocal
提供了一个 withInitial()
方法实现这一功能:
- ThreadLocal<String> initValue = ThreadLocal.withInitial(() -> "initValue");
- public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
- // 返回SuppliedThreadLocal类型对象
- return new SuppliedThreadLocal<>(supplier);
- }
- static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
-
- private final Supplier<? extends T> supplier;
-
- SuppliedThreadLocal(Supplier<? extends T> supplier) {
- this.supplier = Objects.requireNonNull(supplier);
- }
-
- @Override
- protected T initialValue() {
- // 获取初始化值
- return supplier.get();
- }
- }
由图1可知,ThreadLocal.ThreadLocalMap
对应的Entry
中,key为ThreadLocal
对象的弱引用,方法执行对应栈帧中的ThreadLocal
引用为强引用。当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了ThreadLocal
对象的强引用,即表示该ThreadLocal
对象可以被回收了。又因为Entry
中key为ThreadLocal
对象的弱引用,所以当jvm执行GC操作时是能够回收该ThreadLocal
对象的。
而Entry
中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal
对象,是无法释放ThreadLocal.ThreadLocalMap
中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象,这样整个threadlocals
都被回收了。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。
JDK处理的方法是,在ThreadLocalMap
进行set()
、get()
、remove()
的时候,都会进行清理:
- private Entry getEntry(ThreadLocal<?> key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- if (e != null && e.get() == key)
- return e;
- else
- return getEntryAfterMiss(key, i, e);
- }
- private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
-
- while (e != null) {
- ThreadLocal<?> k = e.get();
- if (k == key)
- return e;
- if (k == null)
- //如果key为null,对应的threadlocal对象已经被回收,清理该Entry
- expungeStaleEntry(i);
- else
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
在工作中,我们经常需要维护一些上下文,这样可以避免在方法调用过程中传入过多的参数,需要查询/修改一些数据的时候,直接在当前上下文中操作就行了。举个具体点的例子:当web服务器收到一个请求时,需要解析当前登录态的用户,在后续的业务执行流程中都需要这个用户名。
如果只需要维护一个上下文状态数据还比较好处理,可以通过方法传参的形式,执行每个业务方法的时候都通过添加一个表示用户名方法参数传递进去,但是如果需要维护上下文状态数据比较多的话,这个方式就不太优雅了。
一个可行的方案是通过Threadlocal
实现请求线程的上下文,只要是同一线程的执行过程,不同方法间不传递上下文状态变量,直接操作ThreadLocal
对象实现状态数据的读写。当存在多个上下文状态的话,则需要维护多个ThreadLocal
,似乎也可以勉强接受。但是当遇到业务流程中使用线程池的情况下,从Tomcat传递这些ThreadLocal
到线程池中的线程里就变的比较麻烦了。
基于以上考虑,下面介绍一种基于Threadlocal
实现的上下文管理组件Scope
:
Scope.java
- public class Scope {
-
- // 静态变量,维护不同线程的上下文Scope
- private static final ThreadLocal<Scope> SCOPE_THREAD_LOCAL = new ThreadLocal<>();
-
- // 实例变量,维护每个上下文中所有的状态数据,为了区分不同的状态数据,使用ScopeKey类型的实例作为key
- private final ConcurrentMap<ScopeKey<?>, Object> values = new ConcurrentHashMap<>();
-
- // 获取当前上下文
- public static Scope getCurrentScope() {
- return SCOPE_THREAD_LOCAL.get();
- }
-
- // 在当前上下文设置一个状态数据
- public <T> void set(ScopeKey<T> key, T value) {
- if (value != null) {
- values.put(key, value);
- } else {
- values.remove(key);
- }
- }
-
- // 在当前上下文读取一个状态数据
- public <T> T get(ScopeKey<T> key) {
- T value = (T) values.get(key);
- if (value == null && key.initializer() != null) {
- value = key.initializer().get();
- }
- return value;
- }
-
- // 开启一个上下文
- public static Scope beginScope() {
- Scope scope = SCOPE_THREAD_LOCAL.get();
- if (scope != null) {
- throw new IllegalStateException("start a scope in an exist scope.");
- }
- scope = new Scope();
- SCOPE_THREAD_LOCAL.set(scope);
- return scope;
- }
-
- // 关闭当前上下文
- public static void endScope() {
- SCOPE_THREAD_LOCAL.remove();
- }
- }
ScopeKey.java
- public final class ScopeKey<T> {
-
- // 初始化器,参考 ThreadLocal 的 withInitial()
- private final Supplier<T> initializer;
-
- public ScopeKey() {
- this(null);
- }
-
- public ScopeKey(Supplier<T> initializer) {
- this.initializer = initializer;
- }
-
- // 统一初始化所有线程的 ScopeKey 对应的值,参考 ThreadLocal 的 withInitial()
- public static <T> ScopeKey<T> withInitial(Supplier<T> initializer) {
- return new ScopeKey<>(initializer);
- }
-
- public Supplier<T> initializer() {
- return this.initializer;
- }
-
- // 获取当前上下文中 ScopeKey 对应的变量
- public T get() {
- Scope currentScope = getCurrentScope();
- return currentScope.get(this);
- }
-
- // 设置当前上下文中 ScopeKey 对应的变量
- public boolean set(T value) {
- Scope currentScope = getCurrentScope();
- if (currentScope != null) {
- currentScope.set(this, value);
- return true;
- } else {
- return false;
- }
- }
- }
使用方式
- @Test
- public void testScopeKey() {
- ScopeKey<String> localThreadName = new ScopeKey<>();
-
- // 不同线程中执行时,开启独占的 Scope
- Runnable r = () -> {
- // 开启 Scope
- Scope.beginScope();
- try {
- String currentThreadName = Thread.currentThread().getName();
- localThreadName.set(currentThreadName);
- log.info("currentThread: {}", localThreadName.get());
- } finally {
- // 关闭 Scope
- Scope.endScope();
- }
- };
-
- new Thread(r, "thread-1").start();
- new Thread(r, "thread-2").start();
-
- /** 执行结果
- * [thread-1] INFO com.example.demo.testscope.TestScope - currentThread: thread-1
- * [thread-2] INFO com.example.demo.testscope.TestScope - currentThread: thread-2
- */
- }
-
- @Test
- public void testWithInitial() {
- ScopeKey<String> initValue = ScopeKey.withInitial(() -> "initVal");
-
- Runnable r = () -> {
- Scope.beginScope();
- try {
- log.info("initValue: {}", initValue.get());
- } finally {
- Scope.endScope();
- }
- };
-
- new Thread(r, "thread-1").start();
- new Thread(r, "thread-2").start();
-
- /** 执行结果
- * [thread-1] INFO com.example.demo.testscope.TestScope - initValue: initVal
- * [thread-2] INFO com.example.demo.testscope.TestScope - initValue: initVal
- */
- }
上面的测试用例中在代码中手动开启和关闭Scope
不太优雅,可以在Scope
中添加两个个静态方法包装下Runnable
和Supplier
接口:
- public static <X extends Throwable> void runWithNewScope(@Nonnull ThrowableRunnable<X> runnable)
- throws X {
- supplyWithNewScope(() -> {
- runnable.run();
- return null;
- });
- }
-
- public static <T, X extends Throwable> T
- supplyWithNewScope(@Nonnull ThrowableSupplier<T, X> supplier) throws X {
- beginScope();
- try {
- return supplier.get();
- } finally {
- endScope();
- }
- }
- @FunctionalInterface
- public interface ThrowableRunnable<X extends Throwable> {
- void run() throws X;
- }
-
- public interface ThrowableSupplier<T, X extends Throwable> {
- T get() throws X;
- }
以新的Scope
执行,可以这样写:
- @Test
- public void testRunWithNewScope() {
- ScopeKey<String> localThreadName = new ScopeKey<>();
-
- ThrowableRunnable r = () -> {
- String currentThreadName = Thread.currentThread().getName();
- localThreadName.set(currentThreadName);
- log.info("currentThread: {}", localThreadName.get());
- };
-
- // 不同线程中执行时,开启独占的 Scope
- new Thread(() -> Scope.runWithNewScope(r), "thread-1").start();
- new Thread(() -> Scope.runWithNewScope(r), "thread-2").start();
-
- /** 执行结果
- * [thread-2] INFO com.example.demo.TestScope.testscope - currentThread: thread-2
- * [thread-1] INFO com.example.demo.TestScope.testscope - currentThread: thread-1
- */
- }
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
在上一节中实现的Scope
,通过ThreadLocal
实现了了一个自定义的上下文组件,在同一个线程中通过ScopeKey.set()
/ ScopeKey.get()
读写同一个上下文中的状态数据。
现在需要实现这样一个功能,在一个线程执行过程中开启了一个Scope
,随后使用线程池执行任务,要求在线程池中也能获取当前Scope
中的状态数据。典型的使用场景是:服务收到一个用户请求,通过Scope
将登陆态数据存到当前线程的上下文中,随后使用线程池执行一些耗时的操作,希望线程池中的线程也能拿到Scope
中的登陆态数据。
由于线程池中的线程和请求线程不是一个线程,按照目前的实现,线程池中的线程是无法拿到请求线程上下文中的数据的。
解决方法是,在提交runnable
时,将当前的Scope
引用存到runnable
对象中,当获得线程执行时,将Scope
替换到执行线程中,执行完成后,再恢复现场。在Scope
中新增如下静态方法:
- // 以给定的上下文执行 Runnable
- public static <X extends Throwable> void runWithExistScope(Scope scope, ThrowableRunnable<X> runnable) throws X {
- supplyWithExistScope(scope, () -> {
- runnable.run();
- return null;
- });
- }
-
- // 以给定的上下文执行 Supplier
- public static <T, X extends Throwable> T supplyWithExistScope(Scope scope, ThrowableSupplier<T, X> supplier) throws X {
- // 保留现场
- Scope oldScope = SCOPE_THREAD_LOCAL.get();
- // 替换成外部传入的 Scope
- SCOPE_THREAD_LOCAL.set(scope);
- try {
- return supplier.get();
- } finally {
- if (oldScope != null) {
- // 恢复线程
- SCOPE_THREAD_LOCAL.set(oldScope);
- } else {
- SCOPE_THREAD_LOCAL.remove();
- }
- }
- }
实现支持Scope
切换的自定义线程池ScopeThreadPoolExecutor
:
- public class ScopeThreadPoolExecutor extends ThreadPoolExecutor {
-
- ScopeThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
- TimeUnit unit, BlockingQueue<Runnable> workQueue) {
- super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
- }
-
- public static ScopeThreadPoolExecutor newFixedThreadPool(int nThreads) {
- return new ScopeThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
-
- /**
- * 只要override这一个方法就可以
- * 所有submit, invokeAll等方法都会代理到这里来
- */
- @Override
- public void execute(Runnable command) {
- Scope scope = getCurrentScope();
- // 提交任务时,把执行 execute 方法的线程中的 Scope 传进去
- super.execute(() -> runWithExistScope(scope, command::run));
- }
- }
测试下ScopeThreadPoolExecutor
是否生效:
- @Test
- public void testScopeThreadPoolExecutor() {
- ScopeKey<String> localVariable = new ScopeKey<>();
- Scope.beginScope();
-
- try {
- localVariable.set("value out of thread pool");
- Runnable r = () -> log.info("localVariable in thread pool: {}", localVariable.get());
-
- // 使用线程池执行,能获取到外部Scope中的数据
- ExecutorService executor = ScopeThreadPoolExecutor.newFixedThreadPool(10);
- executor.execute(r);
- executor.submit(r);
-
- } finally {
- Scope.endScope();
- }
-
- /** 执行结果
- * [pool-1-thread-1] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool
- * [pool-1-thread-2] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool
- */
- }
-
- @Test
- public void testScopeThreadPoolExecutor2() {
- ScopeKey<String> localVariable = new ScopeKey<>();
- Scope.runWithNewScope(() -> {
- localVariable.set("value out of thread pool");
- Runnable r = () -> log.info("localVariable in thread pool: {}", localVariable.get());
-
- // 使用线程池执行,能获取到外部Scope中的数据
- ExecutorService executor = ScopeThreadPoolExecutor.newFixedThreadPool(10);
- executor.execute(r);
- executor.submit(r);
- });
-
- /** 执行结果
- * [pool-1-thread-2] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool
- * [pool-1-thread-1] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool
- */
- }
以上两个测试用例,分别通过手动开启Scope
、借助runWithNewScope
工具方法自动开启Scope
两种方式验证了自定义线程池ScopeThreadPoolExecutor
的Scope
可传递性。
接下来介绍如何通过Filter
和Scope
实现Request
粒度的Scope
上下文。思路是:通过注入一个拦截器,在进入拦截器后开启Scope
作为一个请求的上下文,解析Request
对象获取获取相关状态信息(如登陆用户),并在Scope
中设置,在离开拦截器时关闭Scope
。
AuthScope.java
- // 获取登录态的工具类
- public class AuthScope {
- private static final ScopeKey<String> LOGIN_USER = new ScopeKey<>();
-
- public static String getLoginUser() {
- return LOGIN_USER.get();
- }
-
- public static void setLoginUser(String loginUser) {
- if (loginUser == null) {
- loginUser = "unknownUser";
- }
- LOGIN_USER.set(loginUser);
- }
- }
ScopeFilter.java
- @Lazy
- @Order(0)
- @Service("scopeFilter")
- public class ScopeFilter extends OncePerRequestFilter {
-
- @Override
- protected String getAlreadyFilteredAttributeName() {
- return this.getClass().getName();
- }
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
- FilterChain filterChain) throws ServletException, IOException {
- // 开启Scope
- beginScope();
- try {
- Cookie[] cookies = request.getCookies();
- String loginUser = "unknownUser";
- if (cookies != null) {
- for (Cookie cookie : cookies) {
- if (cookie.getName().equals("login_user")) {
- loginUser = cookie.getValue();
- break;
- }
- }
- }
-
- // 设置该 Request 上下文对用的登陆用户
- AuthScope.setLoginUser(loginUser);
-
- filterChain.doFilter(request, response);
- } finally {
- // 关闭Scope
- endScope();
- }
- }
- }
注入Filter
- @Slf4j
- @Configuration
- public class FilterConfig {
-
- @Bean
- public FilterRegistrationBean<ScopeFilter> scopeFilterRegistration() {
- FilterRegistrationBean<ScopeFilter> registration = new FilterRegistrationBean<>();
- registration.setFilter(new ScopeFilter());
- registration.addUrlPatterns("/rest/*");
- registration.setOrder(0);
- log.info("scope filter registered");
- return registration;
- }
- }
UserController.java
- @Slf4j
- @RestController
- @RequestMapping("/rest")
- public class UserController {
-
- // curl --location --request GET 'localhost:8080/rest/getLoginUser' --header 'Cookie: login_user=zhangsan'
- @GetMapping("/getLoginUser")
- public String getLoginUser() {
- return AuthScope.getLoginUser();
- }
-
- // curl --location --request GET 'localhost:8080/rest/getLoginUserInThreadPool' --header 'Cookie: login_user=zhangsan'
- @GetMapping("/getLoginUserInThreadPool")
- public String getLoginUserInThreadPool() {
- ScopeThreadPoolExecutor executor = ScopeThreadPoolExecutor.newFixedThreadPool(4);
- executor.execute(() -> {
- log.info("get login user in thread pool: {}", AuthScope.getLoginUser());
- });
-
- return AuthScope.getLoginUser();
- }
- }
通过以下请求验证,请求线程和线程池线程是否能获取登录态,其中登录态通过Cookie模拟:
- curl --location --request GET 'localhost:8080/rest/getLoginUser' --header 'Cookie: login_user=zhangsan'
- curl --location --request GET 'localhost:8080/rest/getLoginUserInThreadPool' --header 'Cookie: login_user=zhangsan'
github:
https://github.com/pengchengSU/demo-request-scope.git
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
- 文章有帮助的话,在看,转发吧。
- 谢谢支持哟 (*^__^*)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。