JEP 429:范围值(孵化器)
概括
引入_作用域值_,它可以在线程内和线程间共享不可变数据。它们优于线程局部变量,特别是在使用大量虚拟线程时。这是一个正在孵化的API。
目标
-
易用性——提供一个编程模型来在线程内和子线程之间共享数据,从而简化数据流的推理。
-
可理解性——使共享数据的生命周期从代码的语法结构中可见。
-
鲁棒性——确保调用者共享的数据只能由合法的被调用者检索。
-
性能——将共享数据视为不可变,以便允许大量线程共享,并实现运行时优化。
非目标
-
改变 Java 编程语言并不是目标。
-
要求迁移线程局部变量或弃用现有 API 并不是我们的目标
ThreadLocal
。
动机
大型 Java 程序通常由不同且互补的组件组成,这些组件之间需要共享数据。例如,Web 框架可能包括以每个请求线程风格实现的服务器组件和处理持久性的数据访问组件。在整个框架中,用户身份验证和授权依赖于Principal
组件之间共享的对象。服务器组件Principal
为每个处理请求的线程创建一个线程,数据访问组件引用一个线程来Principal
控制对数据库的访问。
下图显示了处理两个请求的框架,每个请求都在自己的线程中。请求处理向上流动,从服务器组件 ( Server.serve(...)
) 到用户代码 ( Application.handle(...)
) 再到数据访问组件 ( DBAccess.open()
)。数据访问组件判断是否允许该线程访问数据库,如下:
-
在线程 1 中,
ADMIN
服务器组件创建的主体允许数据库访问。虚线表示主体将与数据访问组件共享,数据访问组件对其进行检查并继续调用DBAccess.newConnection()
。 -
在线程 2 中,
GUEST
服务器组件创建的主体不允许数据库访问。数据访问组件检查主体,确定用户代码不得继续,并抛出InvalidPrincipalException
.
Thread 1 Thread 2
-------- --------
8. DBAccess.newConnection() 8. throw new InvalidPrincipalException()
7. DBAccess.open() <----------+ 7. DBAccess.open() <----------+
... | ... |
... Principal(ADMIN) ... Principal(GUEST)
2. Application.handle(..) | 2. Application.handle(..) |
1. Server.serve(..) ----------+ 1. Server.serve(..) ----------+
Principal
通常,通过将数据作为方法参数传递来在调用者和被调用者之间共享数据,但这对于服务器组件和数据访问组件之间的共享来说是不可行的,因为服务器组件首先调用不受信任的用户代码。我们需要一种更好的方法来将数据从服务器组件共享到数据访问组件,而不是将其连接到一系列不受信任的方法调用中。
用于共享的线程局部变量
开发人员传统上使用Java 1.2 中引入的_线程局部变量_来帮助组件共享数据,而无需求助于方法参数。线程局部变量是类型的变量ThreadLocal
。尽管看起来像一个普通变量,但线程局部变量有多种化身,每个线程一个;使用的特定化身取决于哪个线程调用其值get()
或set(...)
方法来读取或写入其值。一个线程中的代码自动读取和写入其化身,而另一个线程中的代码自动读取和写入其自己独特的化身。通常,线程局部变量被声明为final
static
字段,以便可以轻松地从许多组件访问它。
下面的示例说明了服务器组件和数据访问组件(两者都在同一请求处理线程中运行)如何使用线程局部变量来共享Principal
.服务器组件首先声明一个线程局部变量PRINCIPAL
(1)。当Server.serve(...)
在请求处理线程中执行时,它将合适的值写入Principal
线程局部变量 (2),然后调用用户代码。如果并且当用户代码调用 时DBAccess.open()
,数据访问组件将读取线程局部变量 (3) 以获取Principal
请求处理线程的 。仅当Principal
指示合适的权限时才允许数据库访问 (4)。
class Server {
final static ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>(); // (1)
void serve(Request request, Response response) {
var level = (request.isAuthorized() ? ADMIN : GUEST);
var principal = new Principal(level);
PRINCIPAL.set(principal); // (2)
Application.handle(request, response);
}
}
class DBAccess {
DBConnection open() {
var principal = Server.PRINCIPAL.get(); // (3)
if (!principal.canOpen()) throw new InvalidPrincipalException();
return newConnection(...); // (4)
}
}
使用线程局部变量可以避免Principal
在服务器组件调用用户代码以及用户代码调用数据访问组件时将 a 作为方法参数传递。线程局部变量充当一种隐藏方法参数:PRINCIPAL.set(...)
调用Server.serve(...)
然后再PRINCIPAL.get()
调用的线程DBAccess.open()
将自动看到它自己的PRINCIPAL
变量化身。实际上,该ThreadLocal
字段充当用于查找Principal
当前线程的值的键。
线程局部变量的问题
不幸的是,线程局部变量有许多无法避免的设计缺陷:
-
无约束的可变性——每个线程局部变量都是可变的:任何可以调用
get()
线程局部变量方法的代码都可以set(...)
随时调用该变量的方法。 APIThreadLocal
允许这样做是为了支持完全通用的通信模型,其中数据可以在组件之间的任何方向流动。然而,这可能会导致类似意大利面条的数据流,并且导致难以辨别哪个组件更新共享状态以及以什么顺序更新的程序。如上例所示,更常见的需求是从一个组件到其他组件的简单单向数据传输。 -
无界生命周期— 一旦通过方法写入了线程局部变量的化身
set(...)
,该化身将在线程的生命周期内保留,或者直到线程中的代码调用该remove()
方法为止。不幸的是,开发人员经常忘记调用remove()
,因此每个线程的数据通常保留的时间超过必要的时间。此外,对于依赖线程局部变量的无约束可变性的程序,可能没有明确的点可以安全地调用线程remove()
;这可能会导致长期内存泄漏,因为在线程退出之前不会对每个线程的数据进行垃圾收集。如果每个线程数据的写入和读取发生在线程执行期间的有限时间内,那就更好了,避免了泄漏的可能性。 -
昂贵的继承——当使用大量线程时,线程局部变量的开销可能会更大,因为父线程的线程局部变量可以被子线程继承。 (事实上,线程局部变量对于一个线程来说并不是局部的。)当开发人员选择创建一个继承线程局部变量的子线程时,子线程必须为先前写入的每个线程局部变量分配存储空间。父线程。这会显着增加内存占用。子线程无法共享父线程使用的存储,因为线程局部变量是可变的,并且
ThreadLocal
API 要求一个线程中的变化在其他线程中不可见。这是不幸的,因为实际上子线程很少set(...)
在其继承的线程局部变量上调用该方法。
迈向轻量级共享
随着虚拟线程的可用性(JEP 425),线程局部变量的问题变得更加紧迫。虚拟线程是JDK实现的轻量级线程。许多虚拟线程共享相同的操作系统线程,从而允许大量虚拟线程。除了数量充足之外,虚拟线程还足够便宜,可以表示任何并发的行为单元。这意味着 Web 框架可以将新的虚拟线程专门用于处理请求的任务,并且仍然能够同时处理数千或数百万个请求。在正在进行的示例中,方法Server.serve(...)
、Application.handle(...)
和DBAccess.open()
都将在每个传入请求的新虚拟线程中执行。
对于这些方法来说,无论它们是在虚拟线程还是传统平台线程中执行,能够共享数据显然都是有用的。因为虚拟线程是 的实例Thread
,所以虚拟线程可以具有线程局部变量;事实上,虚拟线程的短暂非池化性质使得上面提到的长期内存泄漏问题不那么严重。 (当线程快速终止时,调用线程局部变量的remove()
方法是不必要的,因为终止会自动删除其线程局部变量。)但是,如果一百万个虚拟线程中的每一个都有可变的线程局部变量,则内存占用可能会很大。
总之,线程局部变量比共享数据通常所需的复杂性更高,而且成本也无法避免。 Java 平台应该提供一种方法来维护数千或数百万虚拟线程的不可变且可继承的每线程数据。由于这些每线程变量是不可变的,因此子线程可以有效地共享它们的数据。此外,这些每线程变量的生命周期应该受到限制:一旦最初共享数据的方法完成,通过每线程变量共享的任何数据都应该变得不可用。