Java 21 引入了对虚拟线程的支持。与常规 Java 线程(通常对应于 OS 线程)不同,虚拟线程非常轻量,实际上应用程序可以同时创建和使用 100,000 个或更多虚拟线程。
这一魔法是通过对JVM进行两项重大改变来实现的:
虚拟线程由 JVM 而非操作系统管理。如果正在执行,则将其绑定到平台线程(称为载体线程);如果未执行(例如,被阻塞以等待某种形式的通知),则 JVM 会“暂停”虚拟线程并释放载体线程,以便可以调度其他虚拟线程。
平台线程通常会预先分配大约 1 MB 的内存用于其堆栈等。相比之下,虚拟线程的堆栈在堆中进行管理,并且可以只有几百字节 - 可以根据需要增大或缩小。
用于管理虚拟线程之间的协作和通信的 API 与旧平台线程完全相同。这有优点也有缺点:
- 优点:实施者熟悉接口。
- 缺点:您仍然面临多线程应用程序中所有常见的“困难”部分 - 同步块,竞争条件等 - 只是现在问题的数量级增加了。
我们需要一种新方法。这种方法可以充分利用运行数百万虚拟线程的能力,同时使多线程编程更容易。事实上,这样的模型是存在的,50 年前就首次被讨论过:Actors。
Dust与Actor
Actor 概念诞生于 20 世纪 70 年代的麻省理工学院,由 Carl Hewitt 进行研究。Actor 概念是 Erlang 和 Elixir 等语言以及Dust等框架的核心:Java 21+ 版 Actors 的开源(Apache2 许可证)实现。
不同的Actor的实现在细节上有所差异,所以从现在开始我们将描述具体的Dust Actor模型:
- Actor 是与一个虚拟线程关联的Java 对象。
- Actor 有一个“邮箱”,用于接收和排队来自其他 Actor 的消息。线程wait()访问此邮箱,检索消息,处理消息,然后返回等待下一条消息。Actor 如何处理消息称为其行为。
- 请注意,如果 Actor 没有待处理消息,那么由于邮箱线程是虚拟的,JVM 将“停放”Actor 并重用其线程。收到消息后,JVM 将取消停放 Actor 并为其提供一个线程来处理消息。对于只关心消息和行为的开发人员来说,这一切都是透明的。
Actor 可以有自己的可变状态,该状态在 Actor 之外无法访问。在响应收到的消息时,Actor 可以:
- 改变其状态
- 向其他 Actor 发送不可变消息
- 创建或销毁其他 Actor
- 改变其行为
就是这样。请注意,Actor 是单线程的,因此Actor中不存在锁定/同步问题。Actor 影响另一个 Actor 的唯一方法是向其发送不可变消息 - 因此 Actor之间不存在同步问题。
一个 Actor 发送给另一个 Actor 的消息的顺序由接收 Actor 保留,但不保证连续性。如果两个 Actor 同时向同一个 Actor 发送消息,则消息可能会交错,但每个流的顺序都会保留。
Actor 由 ActorSystem 管理。它有一个名称,以及可选的端口号。如果指定了端口,则 ActorSystem 中的 Actor 可以接收远程发送的消息 — 无论是从另一个端口还是从另一个主机。ActorSystem 负责远程情况下消息的(反)序列化。
每个 Actor 都有一个唯一的地址,类似于 URL:dust://host:port/actor-system-name/a1/a2/a3。
如果你与同一个Actor系统中的Actor进行通信,则URL可以简化为:/a1/a2/a3。
但这不仅仅是一个路径名:它表达了 Actors 之间的父/子关系,即:
- 创建了一个名为 a1 的 Actor。
- 然后又创建了一个名为 a2 的行为体:a1 是 a1 的 "父",a2 是 a1 的 "子"。
- 然后,行为体 a2 又创建了自己的子行为体 a3。
Actor 结构
Actors 扩展了 Actor 类。需要注意的是,Actors 不是直接用“new”创建的,而是使用不同的机制。这是建立正确的父子关系所必需的。我们使用该类Props来实现这一点,如以下简单示例所示:
/<strong> |
Actors 是由其Props (见下文)创建的,而Props 也可以包含初始化参数。
因此,在上文中,我们的 PingPongActor 初始化包括一个最大计数,我们很快就会展示它的用途。 Actors 是由其他 Actors 创建的,但这个链条必须从某个地方开始。 当创建 ActorSystem 时,它会创建几个默认的顶级 Actor,包括一个名为 /user 的 Actor。
然后,应用程序可以通过 ActorSystem 创建该 Actor 的子节点:
ActorSystem system = new ActorSystem('PingPong'); |
ActorSystem 的上下文提供了 actorOf() 方法,用于创建 /user Actor 的子节点。 Actor 本身也有一个相同的 actorOf(),用于创建它们的子代。
如果我们现在查看 ActorSystem,我们会看到一个新的 PingPongActor,它的名称是 ping,路径是 /user/ping。 这个创建步骤返回的值是一个 ActorRef--指向该特定 Actor 的 "句柄"。 让我们再创建一个:
ActorRef pong = system.context.actorOf(PingPongActor.props(1000000), ‘pong’);
现在我们有两个 PingPongActor 实例,它们的 "max "状态都设置为 1000000,都在等待接收邮箱中的消息。 有了消息后,它会将消息传递给 createBehavior() lambda,后者会实现我们的行为。
首先,我们需要一个漂亮的消息类来启动程序:
public class PingPongMsg implements Serializable {}
对消息的唯一限制是它们必须是可序列化的。现在让我们看看我们的设置:
ActorSystem system = new ActorSystem('PingPong'); |
ActorRef 有一个 tell() 方法,它接收一个 Serializable 消息对象和一个(可置空)ActorRef。 因此,在上面的代码中,PingPongMsg 的实例被传递给了 pong 的 Actor。 由于第二个参数不是空值,因此该 ActorRef(ping)可在接收者的行为中作为 "发送者 "变量使用。 回想一下,行为中处理 PingPongMsg 的部分是:
case PingPongMsg → { |
这条信息的发送者给了我他的 ActorRef(ping),所以我只是把信息发回给他,通过自变量告诉他我(pong)就是发送者。 如此反复一百万次。 因此,同样的信息在两个 "行为体 "之间总共来回传递了 200 万次,一旦它们的计数器归零,每个 "行为体 "都会自我毁灭。
超越 PingPong
PingPongActor只是能够让人感受到 Actors 和 Dust 的最简单的例子,但显然除此之外价值有限。GitHub 包含几个 Dust 存储库,它们构成了围绕 Dust 框架的小型库。
- dust-core – Dust 的核心:Actors、持久 Actors、用于构建管道的各种结构 Actors、可扩展服务器等。
- dust-http – 小型库,使 Actors 可以轻松访问互联网端点等。
- dust-html – 一个小型库,可让您以惯用的 Dust 轻松操作网页内容
- dust-feeds – 访问 RSS 源、抓取网站并使用 SearXNG 进行网页搜索的攻击者
- dust-nlp – 访问 ChatGPT(及类似)端点和 Hugging Face嵌入API 的参与者
- 使用 LLM 识别和关注热门话题的智能新闻阅读器
- 使用 WiFi 信号强度作为人员代理的建筑物占用管理
- 玩具城的数字孪生 ——8000 名actor只为模拟成群的鸟儿!
- 查找和分析并购活动数据的系统