Java虚拟线程宣告异步编程已死

2022-12-02 0 332

最近,交互式缓存的第三个测试版作为JEP 436的一部分发布。第两个测试版中导入的一些更动已经顺利完成,他们离获得对交互式缓存的完全出访权又近了一步。在责任编辑中,他们将试著为您提供更多有关为何 JVM 生态系中十分须要 Java 交互式缓存的坚实背景知识,主要是为您提供更多认知 Java 交互式缓存的基本知识。

作业系统缓存和网络平台缓存间的校验

目前,在 JDK 中,Java 缓存(也称为“网络平台”缓存)和 OS 线程间存在单对单的关系。

这意味著当缓存正在等待 IO 操作方式顺利完成时,下层作业系统缓存将保持堵塞状态,因而不会被采用,直到该操作方式顺利完成。这在 Java 生态系的可扩展性

各方面一直是两个大难题,即使他们的应用程序受到PS3中需用缓存的管制。

在过去的十年里,他们企图通过采用触发器处置库和采用futures来化解这个难题。例如,采用CompletableFuture他们能实现非堵塞方法,尽管在许多情况下那些数学模型的时效性不是他们所期许的。

触发器程式设计的难题

虽然触发器程式设计是化解缓存管制的可取化解方案,但撰写触发器标识符比次序标识符更复杂。开发者要表述反弹以根据对取值任务的积极响应来应用操作方式,这使得标识符难以认知和逻辑推理。

另两个大难题是增容那些插件显得很十分困难。两个取值的允诺能由多个缓存处置,因而增容、记录或分析栈追踪显得十分十分困难。

在稳定性和可移植性各方面,触发器程式设计也十分有限。他们要放弃某些次序的工作流结构,比如循环式,这意味著一段写出次序的标识符不能轻而易举地转化成触发器的。完全一致的情况发生在相反的情况下。

最后,但同样重要的是,由于接踵而至的复杂程度,撰写隐式触发器标识符更容易手忙脚乱。

高昂的缓存建立

网络平台缓存的另两个难题是它是重对象,建立起来很高昂,因而他们须要事先建立它并将它储存在缓存池中,以避免每天他们须要两个缓存来运转他们的标识符时都建立新缓存。为何它很贵?

Java 缓存的建立是高昂的,即使它涉及为缓存初始化、初始化缓存栈和进行 OS 初始化以注册登记 OS 缓存。

当他们考虑这两个难题时,作业系统缓存的管制和建立网络平台缓存的成本,这意味著他们须要连续函数的缓存池才能安全地运转他们的插件。如果他们不采用连续函数缓存池,他们将面临资源耗尽的风险,并对他们的系统造成严重后果。

高昂的上下文切换

这种设计的另两个难题是上下文切换的代价是多么高昂。当存在上下文切换时,作业系统缓存从两个网络平台缓存切换到另两个网络平台缓存,作业系统要保存当前缓存的本地数据和内存指针,并为新网络平台缓存加载它。上下文切换是一项十分高昂的操作方式,即使它涉及许多 CPU 周期。作业系统负责暂停缓存、保存其栈并分配新缓存,此过程代价高昂,即使它须要加载和卸载缓存栈。

那么他们如何化解那些难题呢?这就是Project Loom及其交互式缓存发挥作用的地方。

救援的交互式缓存Java 中的交互式缓存得名于交互式内存的类比。这是即使他们有一种幻觉,即拥有几乎无限数量的需用缓存(打个比方),这与交互式内存的作用类似。

交互式缓存化解了 JDK 中可伸缩性的主要难题之一,但它是如何化解的呢?答案主要是打破网络平台缓存和作业系统缓存间的关联。

JVM 生态系中的许多插件在达到其 CPU 或内存管制之前就中断了,这主要是由于网络平台缓存和作业系统缓存间的这种校验。建立网络平台缓存十分高昂,因而须要采用缓存池,而且他们总是受到PS3中需用处置单元 (CPU) 数量的管制。

另一各方面,交互式缓存对系统的开销很小,因而,他们的插件中能有数千个。每个交互式缓存都须要两个作业系统缓存来做一些工作,但是,它在等待资源时不会占用作业系统缓存。这意味著交互式缓存能等待 IO,释放它当前采用的网络平台缓存,以便另两个交互式缓存能采用它来做一些工作,并在 IO 操作方式顺利完成后恢复它的工作。

这样做的主要优点是什么?答案之一是廉价的上下文切换!让他们看看为何!

便宜的上下文切换

正如他们之前提到的,上下文切换在 Java 中十分高昂,即使每天发生时都要保存和加载缓存栈。

与交互式缓存不同的是,由于交互式缓存受JVM控制,缓存栈存放在堆内存中,而不是栈中。这意味著为唤醒的交互式缓存分配缓存栈变得更便宜。当交互式缓存被分配给其载体时,将交互式缓存的数据栈加载到“载体”缓存栈的过程称为安装。卸载它的过程称为卸载。

现在让他们简单了解一下缓存调度。

调度

传统的网络平台缓存由作业系统调度,而交互式缓存由 JDK 运转时调度。

同的现有进程间公平(大概)分配 CPU 时间的方式。

另一各方面,交互式缓存由 JDK 运转时直接调度。它的实现方式是在内部采用ForkJoinPool,这是两个用作交互式缓存调度程序的专用池。这意味著ForkJoinPool.commonPool返回的公共池是这个新缓存池的不同实例。

Java虚拟线程宣告异步编程已死

JDK 调度程序不采用时间片,在这种情况下是交互式缓存本身,当它等待堵塞操作方式积极响应时,它会屈服并放弃其载体缓存。这样做的主要结果是他们将有更好的资源利用率,从而增加他们插件的吞吐量。

值得一提的是,下层网络平台缓存,在调度上也称为载体缓存,仍然由OS调度器管理。它现在是两个抽象层,对于撰写并发标识符的开发者来说是完全不可见的。

这里要考虑的另两个各方面是交互式缓存的采用提供更多了并行执行工作的错觉。实际发生的是处置单元时间得到更有效的分配。每个处置单元不会并行执行任何工作,只是更频繁、更便宜地从两个交互式缓存切换到另两个交互式缓存。

既然他们已经了解了交互式缓存是如何工作的,那么他们现在可能会遇到两个难题。每个插件都会从​交互式缓存的导入中受益吗?不是真的,让他们看看为何会这样。

IO 绑定插件

并不是每个插件在采用交互式缓存后都会受益于性能的大幅提升。只有当他们的插件是IO-bound时,他们才会观察到巨大的好处。

这是什么意思?IO-bound 插件是那些花费大量时间等待 IO 操作方式(例如网络初始化、文件或数据库访问)积极响应的插件。那些是当今的大多数插件。

在 IO-bound 插件中采用交互式缓存的好处之所以巨大,是即使交互式缓存十分擅长等待,这意味著缓存能在性能各方面以十分便宜的方式等待和恢复。

在这种情况下,交互式缓存在等待时会堵塞,但网络平台缓存不会。网络平台缓存将被分配到不同的交互式缓存继续做有用的工作而不是等待。这意味著他们的系统将有更好的资源利用率!在下面显示的示例中,他们有两个网络平台缓存,它映射到操作方式系统中相应的作业系统缓存。他们能看到网络平台缓存是如何总是被占用做一些工作,而不是等待 IO 的顺利完成。

每天交互式缓存等待 IO 时,它都会释放其载体缓存。一旦 IO 操作方式顺利完成,交互式缓存将放回ForkJoinPool的 FIFO 队列中,并等待直到有载体缓存需用。

Java虚拟线程宣告异步编程已死

这也意味著他们能在他们的插件中实现吞吐量的大幅增加。交互式缓存通过增加他们能并发处置的任务数量来实现这一点,而不是通过减少延迟来实现。

明确地说,交互式缓存并不比网络平台缓存快,它只是在等待方式和工作分配方式各方面更有效率。

对于受 CPU 管制的插件,他们手头还有其他工具,例如并行任务或ForkJoinPool 中的工作窃取以提高其性能,交互式缓存对其性能的影响最小。请记住两者的区别,以免得到意想不到的结果!

交互式缓存给他们的插件带来了哪些其他好处?有两个十分重要的,他们现在能用同步的方式撰写非堵塞并发标识符。

非堵塞操作方式的同步风格随着 Java 中交互式缓存的导入,撰写并发标识符得到了极大的简化。他们的标识符显得更容易阅读和认知,这是当今触发器程式设计的大难题之一,它的复杂程度有时会失控。

他们现在能撰写并发标识符,而不必处置可能以触发器方式发生的不同交互的编排,JDK 运转时将为他们处置它,在现有交互式缓存中分配需用的 OS 缓存。

如果他们采用 Java 中提供更多的传统并发机制维护旧插件会怎样?

向后兼容性

如果您想知道迁移到 Java 19 后如果标识符采用同步块或任何传统并发机制会发生什么,那么好消息来了。旧的并发标识符将与交互式缓存一起工作,而无需对其进行任何修改。在某些情况下,您甚至可能不须要重新编译和构建新的工件,即使所有那些都由 JDK 运转时处置。在其他情况下,为充分利用交互式缓存所做的更动将是最小的。

目前采用同步块和缓存局部变量有一些管制,他们不会详细介绍,但一般建议是避免采用它。

采用交互式缓存程式设计

在JEP 425下提议的 JDK 中有一些变化。关于如何撰写能够利用交互式缓存的标识符,他们会发现它十分简单。

您能像往常一样撰写标识符,交互式缓存是 JDK 中的内置功能,因而您无需做太多事情即可利用它。

好处之一是交互式缓存从 Thread 类扩展而来,不须要新的缓存类对象。

唯一的变化是他们表述他们建立的缓存是代表交互式缓存还是网络平台缓存的方式。为了实现这一点,JDK 带来了两个Thread.Builder以便能够轻松地实例化和配置两者。

Thread.Builder提供更多了两种实例化缓存的方法。其中之一通过采用Thread.Builder.ofPlatform()方法建立两个传统的网络平台缓存。为了实例化两个交互式缓存,他们将不得不采用Thread.Builder.ofVirtual()。

另两个变化是包含了两个新的ExecutorService,这个新的执行器服务能通过运转Executors.newVirtualThreadPerTaskExecutor()方法来实例化。

让他们通过几个例子看看它是如何工作的!

Executors.newVirtualThreadPerTaskExecutor()这种新方法的导入允许十分容易地从现有的并发标识符转换到交互式缓存。让他们看看下面的例子:

final LongAdder adder = new LongAdder();Runnable task = () -> {try {Thread.sleep(10);System.out.println(“Im running in thread ” + Thread.currentThread());adder.increment();} catch (InterruptedException e) {Thread.interrupted();long start = System.nanoTime();try (ExecutorService executorService = Executors.newCachedThreadPool()) {IntStream.range(1, 10000).forEach(number -> executorService.submit(task));long end = System.nanoTime();System.out.println(“Completed ” + adder.intValue() + ” tasks in ” + (end – start)/1000000 + “ms”);复制

您能看到上面的示例如何采用缓存缓存池提交 10,000 个任务,那些任务模拟了两个小的 IO 操作方式,该操作方式须要 10 毫秒加上打印到控制台和递增计数器所花费的时间。

如果他们运转这段标识符,他们会得到以下结果:

Im running in thread Thread[#1271,pool-1-thread-1242,5,main]Im running in thread Thread[#1260,pool-1-thread-1231,5,main]Im running in thread Thread[#928,pool-1-thread-899,5,main]Im running in thread Thread[#275,pool-1-thread-246,5,main]Completed 9999 tasks in 4740ms复制

为了简洁起见,他们只包含最新的元素和最终结果,您能看到他们如何采用缓存缓存池中的网络平台缓存。运转如此简单的程序须要 4.7 秒。

让他们看看当他们使用新的交互式缓存执行器时会发生什么:

long start = System.nanoTime();try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(1, 10000).forEach(number -> executor.submit(task));long end = System.nanoTime();System.out.println(“Completed ” + adder.intValue() + ” tasks in ” + (end – start)/1000000 + “ms”);复制

您会注意到,切换到交互式缓存就像切换到新的执行程序服务一样简单,其余标识符保持不变!这太棒了,对吧?

采用交互式缓存的性能如何?那些是他们得到的结果:

Im running in thread VirtualThread[#10029]/runnable@ForkJoinPool-1-worker-10Im running in thread VirtualThread[#10031]/runnable@ForkJoinPool-1-worker-10Im running in thread VirtualThread[#10027]/runnable@ForkJoinPool-1-worker-10Im running in thread VirtualThread[#10028]/runnable@ForkJoinPool-1-worker-10Completed 9999 tasks in 760ms

交互式缓存只用了 760 毫秒!这是为何?正如他们之前看到的,网络平台缓存在交互式缓存等待 IO 操作方式时不会被堵塞,因而在交互式缓存等待时能处置其他任务。这对 JVM 生态系来说意义重大!

Thread.ofVirtual()

现在让他们看两个类似的例子,在这种情况下,他们将采用Thread.ofPlatform()和Thread.ofVirtual()来指定他们将在测试中采用哪种缓存。

他们将Thread.ofPlatform()首先运转两个示例:

long start = System.nanoTime();int[] numbers = IntStream.range(1, 10000).toArray();List threads = Arrays.stream(numbers).mapToObj(num ->Thread.ofPlatform().start(task)).toList();threads.parallelStream().forEach(thread -> {try {thread.join();} catch (InterruptedException e) {throw new RuntimeException(e);long end = System.nanoTime();System.out.println(“Completed ” + adder.intValue() + ” tasks in ” + (end – start)/1000000 + “ms”);复制

他们启动 9,999 个缓存来运转我们在上两个示例中采用的相同任务,然后他们采用 等待它顺利完成join()。

如果他们运转此测试,大约须要 2-3 秒才能顺利完成。

如果他们采用相同的示例但只是实例化交互式缓存会怎样?

List threads = Arrays.stream(numbers).mapToObj(num ->Thread.ofVirtual().start(task)).toList();

正如他们在前面的示例中观察到的,交互式缓存提供更多了更好的吞吐量,如下面的结果所示。

Im running in thread VirtualThread[#10029]/runnable@ForkJoinPool-1-worker-4Im running in thread VirtualThread[#10030]/runnable@ForkJoinPool-1-worker-4Im running in thread VirtualThread[#10031]/runnable@ForkJoinPool-1-worker-4Im running in thread VirtualThread[#9954]/runnable@ForkJoinPool-1-worker-5Completed 9999 tasks in 722ms

同样,交互式缓存以相当大的差异击败了网络平台缓存。

请记住,从他们没有运转正确基准的意义上说,那些时间安排并不准确。他们没有预热 JVM 来给 JIT 编译器时间来执行改进,他们也运转两个单一的执行。这只是为了向您展示他们的吞吐量能通过交互式缓存提高多少!

他们想提及的最后一件事是,交互式缓存还打开了在 Java 中导入结构化并发的大门。当在不同的嵌套级别运转多个并发任务时,此更动还将使 Java 标识符更加安全。

Java 将很快导入结构化并发和称为作用域的东西作为JEP 429的一部分,这与 Kotlin 在其[url=https://kotlinlang.org/docs/coroutines-overview.html]协程[/url]中所做的十分相似。

结论

在责任编辑中,他们了解了交互式缓存将如何化解 Java 生态系中的主要难题之一。由于PS3中作业系统缓存数量的管制,作业系统缓存和网络平台缓存间的现有校验对于某些插件来说是两个巨大的管制因素。

长期以来,触发器程式设计一直是他们的救世主,但是,他们看到交互式缓存是导致他们所知道的触发器程式设计死亡的两个重要因素。在下两个 JDK 版本中提供更多此更动后,将采用简单的并发范例。

更多交互式缓存:Loom交互式缓存

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务