-
ThreadLocal -
基本概念 -
应用案例 -
StructuredTaskScope -
ScopedValue
-
基本概念 -
基本用法 -
源码分析 -
小结 ThreadLocal是一种实现将变量在各线程之间隔离的方案,也叫线程局部变量表。在Java中每个线程都拥有一个ThreadLocal下的ThreadLocalMap类型的变量,它用来存储定义在线程中的ThreadLocal对象,ThreadLocalMap的键是一个弱引用,指向对应的ThreadLocal对象。
但值得每一位Java开发者注意的是ThreadLocal变量如果不及时
remove()
会造成严重的内存泄露问题。在JDK 20 Early-Access Build 28版本中便针对ThreadLocal类重新设计了一个ScopedValue类。ScopedValue是一个JDK孵化功能在已发布的JDK20版本中需要手动配置才能使用,ScopedValue的作用是在某些情况下作为ThreadLocal的替代。在同一线程上运行的不同代码可以通过ScopedValue共享不可变的值。ScopedValue主要是为了解决虚拟线程使用ThreadLocal时可能存在的一些问题。
在本期文章中讲会介绍几个ThreadLocal在开发实战中的案例背景以及详细介绍在JDK19中提出的新的并发工具和JDK20正在孵化的ScopedValue类。
在引言中已经大致地介绍了一部分关于ThreadLocal的概念,ThreadLocal存在的意义不仅仅是为了实现隔离更重要的是为了解决对象的复用问题,这些思想在数据库连接池框架中都有体现。但是ThreadLocal又会导致内存泄漏问题,这是由于ThreadLocalMap中的ThreadLocal对象没有被JVM及时回收导致的,为了解决这个问题而使用了弱引用WeakReference,但是弱引用的ThreadLocal被设置为null后不及时通过remove方法来清理也同样会导致内存泄漏问题。
以最常见的Spring应用为例,ThreadLocal在这些应用中完全可以大做文章。
在一些电商项目的Spring业务中会需要对每个请求进行线程隔离:
这段代码中,
ShoppingCartService
是一个 Spring Bean,用来管理购物车信息。在这个 Bean 里,使用了ThreadLocal<ShoppingCart>
来保存每个线程的购物车信息。getCurrentCart
方法首先会从ThreadLocal
中获取购物车信息,如果当前线程没有对应的购物车信息,那么就创建一个新的购物车,并保存到ThreadLocal
中。checkout
方法用来执行结账操作,结账完成后,需要通过cartHolder.remove();
清除当前线程中的购物车信息,以防止内存泄露。这样,即使在多线程环境下,每个线程都有自己独立的购物车信息,互不影响。这就是ThreadLocal
在解决 Spring Bean 线程安全问题上的一个应用场景。在业务逻辑中使用ThreadLocal是很常见的一种处理线程隔离数据的方法。我们不妨思考,如果一系列接口都需要先进行用户认证,然后再操作这个用户数据应该怎么做?这个问题是非常简单的,使用Spring Security整合JWT对前端传递的Token进行解析得到用户名后再校验用户即可。这完全可以封装成一个切面来处理,至于业务中又需要用到这个用户只需要再从Spring Security中去取就行了。但是一些业务会在其切面中对用户数据进行一些预处理,如更新访问接口时间戳。那么这再从Spring Security中去取就显得不妥了,因为这可能会导致和想要得到的对象预期不符。那么该怎么做呢?很显然是使用ThreadLocal来缓存这个用户对象,让这个User在整个http session过程中都处于仅存的一份状态:
在业务中使用这个切面以及
UserConsistencyAspect .getUser()
方法就可以获取到这个http session中的User对象了。在解决Spring Bean的线程不安全问题时会使用到ThreadLocal来保障Bean的线程安全:
在很多情况下,开发者会使用Spring来管理数据库的会话或者事务,但是这样的Bean通常是线程不安全的,比如 Hibernate的SessionFactory或者MyBatis的SqlSessionFactory。这些工厂产生的Session是线程不安全的。在电商项目中,一个常见的场景是,可能会在一个请求处理的过程中需要多次和数据库进行交互。这个时候,为了保证在一个请求中使用同一个数据库会话(Session),通常会把这个 Session 放在一个ThreadLocal中。这样,即使在一个线程中的不同方法里,也可以获取到同一个Session。在这个例子中,每个线程都有自己的Session实例,存储在ThreadLocal中。不同的线程调用
getSession()
方法时,都会从ThreadLocal中获取到属于自己的Session。但是事实上这些session的处理已经在mybatis或hibernate中都已经通过ThreadLocal处理好了不需要开发者再在业务中对session进行隔离。这里的例子主要是为了解释 ThreadLocal 是如何工作的,并不是实际开发中推荐的做法。结构化并发编程式(Structured Concurrent)和虚拟线程(Virtual Threads)息息相关。要了解ScopedValue就必须先了解这两个概念,自JDK5以来一直保持着这样一种理念:我们不应该直接与线程交互。正确的模式是将任务作为Runnable或Callable提交给ExecutorService或Executor,然后对返回的Future进行操作。Loom项目中一直保留了这种模型,并添加了一些不错的功能。这里要介绍的第一个对象是Scope对象, 确切的类型是StructuredTaskScope。我们可以把这个对象看做一个虚拟线程启动器,我们以
Callable
的形式向它提交任务,我们将得到一个future
返回,并且这个callable
将在由作用域Scope
为我们创建的虚线程种执行。这很像Executor
。但二者之间也有很大的区别。StructuredTaskScope实例是AutoCloseable(自动关闭)的,我们可以使用
try-with-resource
模式。通过fork()
方法fork一个Callable类型的任务,fork()
方法返回一个Future对象,我们调用join()方法阻塞调用,它将阻塞当前线程,直到所有提交(frok)给StructuredTaskScope的任务都完成。最后调用Future的resultNow()
获取结果并返回。resultNow()
将抛出异常,如果我们在Future完成前调用它,所以我们要在join()
方法中调用并将其返回。
没有回复内容