与结构化并发更相关的应该是JDK8提出的CompletableFuture,我会在下一期番外中进一步地介绍它们。ScopedValue是基于结构化并发理念在JDK20中被孵化的一个功能,它显然不是为了取代ThreadLocal出现的,而是能让结构化并发中的虚拟线程也能各自享有外部的变量。其实结构化并发中也可以使用ThreadLocal,但是ThreadLocal本身存在一些很大的问题:
-
ThreadLocal变量是可变的,任何运行在当前线程中的代码都可以修改该变量的值,很容易产生一些难以调试的bug。 -
ThreadLocal变量的生命周期会很长。当使用ThreadLocal变量的 set
方法,为当前线程设置了值之后,这个值在线程的整个生命周期中都会保留,直到调用remove
方法来删除。但是绝大部分开发人员不会主动调用remove
来进行删除,这可能造成内存泄漏。 -
ThreadLocal变量可以被继承。如果一个子线程从父线程中继承ThreadLocal变量,那么该子线程需要独立存储父线程中的全部ThreadLocal变量,这会产生比较大的内存开销。
虚拟线程的特点是数量巨大,但是每个虚拟线程的生命周期较短,因此不容易产生内存泄漏问题。但是线程继承所带来的内存开销会更大。为了解决这些问题便孵化了ScopedValue,ScopedValue具备ThreadLocal的核心特征,也就是每个线程只有一个值。与ThreadLocal不同的是,ScopedValue是不可变的,并且有确定的作用域,这也是名字中scoped的含义。
ScopedValue对象用jdk.incubator.concurrent
包中的ScopedValue
类来表示。使用ScopedValue的第一步是创建ScopedValue
对象,通过静态方法newInstance
来完成,ScopedValue对象一般声明为static final
。由于ScopedValue是孵化功能,要想使用需要在项目的第一级包目录的同级目录中创建一个java类module-info.java
来将其引入模块中:
同时需要再启动参数VM Option
中启用预览功能--enable-preview
。下一步是指定ScopedValue
对象的值和作用域,通过静态方法where
来完成。where
方法有 3 个参数:
-
ScopedValue
对象 -
ScopedValue
对象所绑定的值 -
Runnable
或Callable
对象,表示ScopedValue
对象的作用域
在Runnable
或Callable
对象执行过程中,其中的代码可以用ScopedValue
对象的get
方法获取到where
方法调用时绑定的值。这个作用域是动态的,取决于Runnable
或Callable
对象所调用的方法,以及这些方法所调用的其他方法。当Runnable
或Callable
对象执行完成之后,ScopedValue
对象会失去绑定,不能再通过get
方法获取值。在当前作用域中,ScopedValue
对象的值是不可变的,除非再次调用where
方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。使用作用域值有以下几个优势:
-
提高数据安全性:由于作用域值只能在当前范围内访问,因此可以避免数据泄露或被恶意修改。 -
提高数据效率:由于作用域值是不可变的,并且可以在线程之间共享,因此可以减少数据复制或同步的开销。 -
提高代码清晰度:由于作用域值只能在当前范围内访问,因此可以减少参数传递或全局变量的使用。
Java JEP 429是一个正在孵化的新特性,它提供了一种在线程内部和线程之间共享不可变数据的方式。目前,Java JEP 429 还处于孵化器阶段,并没有被正式纳入 Java 语言规范。
这段代码展示了如何使用ScopedValue
和结构化并发来创建并执行多个并行任务,并安全地传递和操作任务上下文中的值。
A value that is set once and is then available for reading for a bounded period of execution by a thread. A ScopedValue allows for safely and efficiently sharing data for a bounded period of execution without passing the data as method arguments. ScopedValue defines the where(ScopedValue, Object, Runnable) method to set the value of a ScopedValue for the bouned period of execution by a thread of the runnable’s run method. The unfolding execution of the methods executed by run defines a dynamic scope. The scoped value is bound while executing in the dynamic scope, it reverts to being unbound when the run method completes (normally or with an exception). Code executing in the dynamic scope uses the ScopedValue get method to read its value. Like a thread-local variable, a scoped value has multiple incarnations, one per thread. The particular incarnation that is used depends on which thread calls its methods.
在开始ScopedValue的源码分析之前,先看一下Java doc的介绍:ScopedValue
是一个对象,它被设置一次后,在执行期间由一个线程有限期地读取。ScopedValue
允许在有限的执行期间内在不将数据作为方法参数传递的情况下安全、有效地共享数据。ScopedValue
定义了 where(ScopedValue, Object, Runnable)
方法,这个方法在一个线程执行 runnable 的 run 方法的有限执行期间内设置 ScopedValue
的值。由 run 执行的方法展开执行定义了一个动态作用域。在动态作用域中执行时,作用域值是绑定的,当 run 方法完成时(正常或异常),它恢复到未绑定状态。在动态作用域中执行的代码使用 ScopedValue
的 get 方法来读取其值。与线程局部变量类似,作用域值有多个化身,每个线程一个。使用哪个化身取决于哪个线程调用其方法。ScopedValue
的一个典型用法是在 final 和 static 字段中声明。字段的可访问性将决定哪些组件可以绑定或读取其值。ScopedValue中有3个内部类,分别是Snapshot、Carrier、Cache,他们在ScopedValue中起着至关重要的角色。
Snapshot
An immutable map from ScopedValue to values. Unless otherwise specified, passing a null argument to a constructor or method in this class will cause a NullPointerException
to be thrown.
Snapshot是一个从ScopedValue到值的不可变映射。除非特别说明,否则将null参数传递给这个类的构造器或方法会导致抛出NullPointerException
异常。这个类的主要用途是为ScopedValue实例创建一个不可变的映射,这样在运行时,无论其它代码如何修改原始的ScopedValue实例,Snapshot中的值都不会发生变化。它为了提供一个安全的方式来在多线程环境下共享值。
Carrier
A mapping of scoped values, as keys, to values. A Carrier is used to accumlate mappings so that an operation (a Runnable
or Callable
) can be executed with all scoped values in the mapping bound to values. The following example runs an operation with k1 bound (or rebound) to v1, and k2 bound (or rebound) to v2. ScopedValue.where(k1, v1).where(k2, v2).run(() -> ... );
A Carrier is immutable and thread-safe. The where method returns a new Carrier object, it does not mutate an existing mapping. Unless otherwise specified, passing a null argument to a method in this class will cause a NullPointerException
to be thrown.
Carrier类用于累积映射,以便可以执行一个操作(Runnable
或Callable
),在该操作中,映射中的所有scoped values都绑定到值。Carrier是不可变的,并且是线程安全的。where
方法返回一个新的Carrier对象,不会改变现有的映射。这是用于在ScopedValue实例和对应值之间创建和保持映射关系的工具,使得这些映射关系可以在执行操作时被一并应用。
Cache
A small fixed-size key-value cache. When a scoped value’s get() method is invoked, we record the result of the lookup in this per-thread cache for fast access in future.
Cache是一个小型的固定大小的键值缓存。当调用一个scoped value的get()
方法时,我们在这个每线程缓存中记录查找的结果,以便在将来快速访问。这个类的主要作用是优化性能。通过缓存get()
方法的结果,可以避免在多次获取同一个ScopedValue的值时进行重复的查找操作。只有当ScopedValue的值被更改时,才需要更新缓存。
where()
where()
方法是ScopedValue类的核心方法与入口,它接收三个参数。当操作完成时(正常或出现异常),ScopedValue将在当前线程中恢复为未绑定状态,或恢复为先前绑定时的先前值。
作用域值旨在以结构化方式使用。如果op
已经创建了一个StructuredTaskScope但没有关闭它,那么退出op会导致在动态范围内创建的每个StructuredTaskScope被关闭。这可能需要阻塞,直到所有子线程都完成了它们的子任务。关闭是按照创建它们的相反顺序完成的。
使用ScopedValue.where(key, value, op);
等价于使用ScopedValue.where(key, value).call(op);
这个方法会将前两个参数委派给Carrier.of(key, value);
方法
在Carrier类中where方法会返回一个新的Carrier对象,这是一种责任链的设计模式
call()
where方法主要是构建Carrier对象,而后这些都会委派给后续的Carrier中的call方法来实现对Callable的一个调用。调用关系如下:
call方法调用链的方法中有很多细节是关于处理Snapshot和Cache的,这些内容可能在将来的Java版本中发生变化这里就不再赘述。
ThreadLocal与ScopedValue在Java并发编程中都起着至关重要的作用,他们分别适用于不同的场景,开发人员需要根据具体需求来选择使用。ThreadLocal主要用于普通并发编程。在Java中,每个线程都有自己的栈,栈中存储的是这个线程需要的局部变量。ThreadLocal则提供了一个独特的机制,使每个线程都可以拥有自己独立的一份数据,其他线程无法访问。这种机制非常适用于那些在处理并发编程中需要隔离线程状态或者实现线程间数据隔离的场景,例如数据库连接、Session会话等。
然而,ThreadLocal虽然能够实现线程级别的数据隔离,但它本身并不能解决更复杂的并发问题,例如异步任务的并发控制、异步任务之间的数据共享等问题。这就需要一种新的工具来解决,即ScopedValue。
ScopedValue是Java引入的新特性,它是为了支持结构化并发编程而设计的。结构化并发允许开发人员通过定义并发的结构,对并发程序的生命周期进行管理。ScopedValue提供了一种方法,使得一个值可以在一个定义好的执行范围(也就是一个“scope”)内,被并发任务共享。
在结构化并发编程中,ScopedValue主要用于实现并发任务间的数据共享,和ThreadLocal相比,ScopedValue可以更好地控制并发任务之间的数据共享,同时也可以更好地对并发任务的生命周期进行管理。例如,一个线程可以将一个值放入ScopedValue中,然后在该线程启动的所有子线程中都可以访问这个值。这样可以避免在异步并发任务中传递大量参数,简化了并发编程。
没有回复内容