关于前端:Java-19-发布Loom-怎么解决-Java-的并发模型缺陷丨未来源码

5次阅读

共计 7750 个字符,预计需要花费 20 分钟才能阅读完成。

​ 

本文来自 InfoQ 中文站,原作者 Deepu K Sasidharan。

举荐语:

对于咱们开发的网站,如果访问量太大,申请激增,就须要思考相干的并发问题。异步并发,意味着要适应更简单的编程格调。Java 中的传统线程十分沉重,并且与操作系统线程一对一绑定。而 Loom 是 jave 生态里一个较新的我的项目,它试图解决传统并发模型中的限度。但具体怎么实现,本文做了具体的阐明。

随着 Project Loom 的进入,兴许将来,java 生态系统的性能将产生数量级晋升的翻天覆地的变动。

—— MobTech 袤博科技资深 java 开发工程师  零零发

Java 19 曾经于日前公布,其中最引人注目的个性就要数虚构线程了,本文介绍了 Loom 我的项目中虚构线程和结构化编程的基础知识,并将其与操作系统线程进行了比照剖析。

Java 在其倒退晚期就具备良好的多线程和并发能力,可能高效地利用多线程和多核 CPU。Java 开发工具包(Java Development Kit,JDK)1.1 对平台线程(或操作系统(OS)线程)提供了根本的反对,JDK 1.5 提供了更多的实用工具和更新,以改善并发和多线程。JDK 8 带来了异步编程反对和更多的并发改善。尽管在多个不同的版本中都进行了改良,但在过来三十多年中,除了基于操作系统的并发和多线程反对之外,Java 并没有任何突破性的停顿。

只管 Java 中的并发模型十分弱小和灵便,但它并不是最易于应用的,而且开发人员的体验也不是很好。这次要是因为它默认应用的共享状态并发模型。咱们必须借助同步线程来防止数据竞争(data race)和线程阻塞这样的问题。我已经在一篇名为“古代编程语言中的并发:Java”的博客文章中探讨过 Java 并发问题。

Loom 我的项目是什么?

Loom 我的项目致力于大幅缩小编写、保护和察看高吞吐量并发利用相干的工作,以最佳的形式利用现有的硬件。——Ron Pressler(Loom 我的项目的技术负责人)

操作系统线程是 Java 并发模型的外围,围绕它们有一个十分成熟的生态系统,然而它们也有一些毛病,如计算形式很低廉。咱们来看一下并发的两个最常见应用场景,以及以后的 Java 并发模型在这些场景下的毛病。

最常见的并发应用场景之一就是借助服务器在网络上为申请提供服务。在这样的场景中,首选的办法是“每个申请一个线程(thread-per-request)”模型,即由一个独自的线程解决每个申请。这种零碎的吞吐量能够用 Little 定律来计算,该定律指出,在一个稳固的零碎中,均匀并发量(服务器并发解决的申请数)L 等于吞吐量(申请的均匀速率)λ 乘以提早(解决每个申请的均匀工夫)W。基于此,咱们能够得出,吞吐量等于均匀并发除以提早(λ = L/W)。

因而,在“每个申请一个线程”模型中,吞吐量将受到操作系统线程数量的限度,这取决于硬件上可用的物理外围 / 线程数。为了解决这个问题,咱们必须应用共享线程池或异步并发,这两种办法各有毛病。线程池有很多限度,如线程透露、死锁、资源激增等。异步并发意味着必须要适应更简单的编程格调,并审慎解决数据竞争。它们还有可能呈现内存透露、线程锁定等问题。

另一个常见的应用场景是并行处理或多线程,咱们可能会把一个工作分成跨多个线程的子工作。此时,咱们必须编写防止数据损坏和数据竞争的解决方案。在有些状况下,当执行散布在多个线程上的并行任务时,还必须要确保线程同步。这种实现会十分软弱,并且将大量的责任推给了开发人员,以确保没有像线程泄露和勾销提早这样的问题。

Loom 我的项目旨在通过引入两个新个性来解决以后并发模型中的这些问题,即虚构线程(virtual thread)和结构化并发(structured concurrency)。

虚构线程

Java 19 曾经于 2022 年 9 月 20 日公布,虚构线程是其中的一项预览性能。

虚构线程是轻量级的线程,它们不与操作系统线程绑定,而是由 JVM 来治理。它们实用于“每个申请一个线程”的编程格调,同时没有操作系统线程的限度。咱们可能创立数以百万计的虚构线程而不会影响吞吐。这与 Go 编程语言(Golang)的协程(如 goroutines)十分类似。

Java 19 中的虚构线程新个性很易于应用。在这里,我将其与 Golang 的 goroutines 以及 Kotlin 的 coroutines 进行了比照。

虚构线程

Thread.startVirtualThread(() -> {    System.out.println("Hello, Project Loom!");});

Goroutine

go func() {    println("Hello, Goroutines!")}()

Kotlin coroutine

runBlocking {launch {        println("Hello, Kotlin coroutines!")    }}

冷常识:在 JDK 1.1 之前,Java 已经反对过绿色线程(又称虚构线程),但该性能在 JDK 1.1 中移除了,因为过后该实现并没有比平台线程更好。

虚构线程的新实现是在 JVM 中实现的,它将多个虚构线程映射为一个或多个操作系统线程,开发人员能够按需应用虚构线程或平台线程。这种虚构线程实现还有如下几个

注意事项:

  • 在代码、运行时、调试器和分析器(profiler)中,它是一个 Thread。
  • 它是一个 Java 实体,并不是对原生线程的封装。
  • 创立和阻塞它们是代价低廉的操作。
  • 它们不应该放到池中。
  • 虚构线程应用了一个基于工作窃取(work-stealing)的 ForkJoinPool 调度器。
  • 能够将可插拔的调度器用于异步编程中。
  • 虚构线程会有本人的栈内存。
  • 虚构线程的 API 与平台线程十分类似,因而更容易应用或移植。

咱们看几个展现虚构线程威力的样例。

线程的总数量

首先,咱们看一下在一台机器上能够创立多少个平台线程和虚构线程。我的机器是英特尔酷睿 i9-11900H 处理器,8 个外围、16 个线程、64GB 内存,运行的操作系统是 Fedora 36。

平台线程

var counter = new AtomicInteger();
while (true) {new Thread(() -> {int count = counter.incrementAndGet();
        System.out.println("Thread count =" + count);
        LockSupport.park();}).start();}

在我的机器上,在创立 32,539 个平台线程后代码就解体了。

虚构线程

var counter = new AtomicInteger();
while (true) {Thread.startVirtualThread(() -> {int count = counter.incrementAndGet();
        System.out.println("Thread count =" + count);
        LockSupport.park();});
}

在我的机器上,过程在创立 14,625,956 个虚构线程后被挂起,但没有解体,随着内存逐步可用,它始终在迟缓进行。你可能想晓得为什么会呈现这种状况。这是因为被 park 的虚构线程会被垃圾回收,JVM 可能创立更多的虚构线程并将其调配给底层的平台线程。

工作吞吐量

咱们尝试应用平台线程来运行 100,000 个工作。

try (var executor = Executors.newThreadPerTaskExecutor(Executors.defaultThreadFactory())) {IntStream.range(0, 100_000).forEach(i -> executor.submit(() -> {Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
    }));
}

在这里,咱们应用了带有默认线程工厂的 newThreadPerTaskExecutor 办法,因而应用了一个线程组。运行这段代码并计时,我失去了如下的后果。当应用 Executors.newCachedThreadPool() 线程池时,我失去了更好的性能。

# 'newThreadPerTaskExecutor' with 'defaultThreadFactory'
0:18.77 real,   18.15 s user,   7.19 s sys,     135% 3891pu,    0 amem,         743584 mmem
# 'newCachedThreadPool' with 'defaultThreadFactory'
0:11.52 real,   13.21 s user,   4.91 s sys,     157% 6019pu,    0 amem,         2215972 mmem

看着还不错。当初,让咱们用虚构线程实现雷同的工作。​​​​​​​

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(0, 100_000).forEach(i -> executor.submit(() -> {Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
    }));
}

运行这段代码并计时,我失去了如下后果:

0:02.62 real,   6.83 s user,    1.46 s sys,     316% 14840pu,   0 amem,         350268 mmem

这比基于平台线程的线程池要好得多。当然,这些都是很简略的应用场景,线程池和虚构线程的实现都能够进一步优化以取得更好的性能,但这不是这篇文章的重点。

用同样的代码运行 Java Microbenchmark Harness(JMH),失去的后果如下。能够看到,虚构线程的性能比平台线程要好很多。

# Throughput
Benchmark                             Mode  Cnt  Score   Error  Units
LoomBenchmark.platformThreadPerTask  thrpt    5  0.362 ± 0.079  ops/s
LoomBenchmark.platformThreadPool     thrpt    5  0.528 ± 0.067  ops/s
LoomBenchmark.virtualThreadPerTask   thrpt    5  1.843 ± 0.093  ops/s

# Average time
Benchmark                             Mode  Cnt  Score   Error  Units
LoomBenchmark.platformThreadPerTask   avgt    5  5.600 ± 0.768   s/op
LoomBenchmark.platformThreadPool      avgt    5  3.887 ± 0.717   s/op
LoomBenchmark.virtualThreadPerTask    avgt    5  1.098 ± 0.020   s/op

你能够在 GitHub 上找到该基准测试的源代码。如下是其余几个有价值的虚构线程基准测试:

  • 在 GitHub 上,Elliot Barlas 应用 ApacheBench 做的一个乏味的基准测试。
  • Alexander Zakusylo 在 Medium 上应用 Akka actors 的基准测试。
  • 在 GitHub 上,Colin Cachia 做的 I/O 和非 I/O 工作的 JMH 基准测试。

结构化并发

结构化并发是 Java 19 中的一个孵化性能。

结构化并发的目标是简化多线程和并行编程。它将在不同线程中运行的多个工作视为一个工作单元,简化了错误处理和工作勾销,同时进步了可靠性和可观测性。这有助于防止线程透露和勾销提早等问题。作为一个孵化性能,在稳固过程中可能会经验进一步的变更。

咱们思考如下这个应用 java.util.concurrent.ExecutorService 的样例。void handleOrder() throws ExecutionException, InterruptedException {

void handleOrder() throws ExecutionException, InterruptedException {try (var esvc = new ScheduledThreadPoolExecutor(8)) {Future<Integer> inventory = esvc.submit(() -> updateInventory());
        Future<Integer> order = esvc.submit(() -> updateOrder());

        int theInventory = inventory.get();   // Join updateInventory
        int theOrder = order.get();           // Join updateOrder

        System.out.println("Inventory" + theInventory + "updated for order" + theOrder);
    }
}

咱们心愿 updateInventory() 和 updateOrder() 这两个子工作可能并发执行。每一个工作都能够独立地胜利或失败。现实状况下,如果任何一个子工作失败,handleOrder() 办法都应该失败。然而,如果某个子工作产生失败的话,事件就会变得难以预料。

  • 构想一下,updateInventory() 失败并抛出了一个异样。那么,handleOrder() 办法在调用 invent.get() 时将会抛出异样。到目前为止,还没有什么大问题,但 updateOrder() 呢?因为它在本人的线程上运行,所以它可能会胜利实现。然而当初咱们就有了一个库存和订单不匹配的问题。假如 updateOrder() 是一个代价昂扬的操作。在这种状况下,咱们白白浪费了资源,不得不编写某种防护逻辑来撤销对订单所做的更新,因为咱们的整体操作曾经失败。
  • 假如 updateInventory() 是一个代价昂扬的长时间运行操作,而 updateOrder() 抛出一个谬误。即使 updateOrder() 抛出了谬误,handleOrder() 工作仍然会在 inventory.get() 办法上阻塞。现实状况下,咱们心愿 handleOrder() 工作在 updateOrder() 产生故障时勾销 updateInventory(),这样就不会浪费时间了。

  • 如果执行 handleOrder() 的线程被中断,那么中断不会被流传到子工作中。在这种状况下,updateInventory() 和 updateOrder() 会泄露并持续在后盾运行。

对于这些场景,咱们必须小心翼翼地编写变通计划和故障防护措施,把所有的职责推到了开发人员身上。

咱们能够应用上面的代码,用结构化并发实现同样的性能。

void handleOrder() throws ExecutionException, InterruptedException {try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {Future<Integer> inventory = scope.fork(() -> updateInventory());
        Future<Integer> order = scope.fork(() -> updateOrder());

        scope.join();           // Join both forks
        scope.throwIfFailed();  // ... and propagate errors

        // Here, both forks have succeeded, so compose their results
        System.out.println("Inventory" + inventory.resultNow() + "updated for order" + order.resultNow());
    }
}

与之前应用 ExecutorService 的样例不同,咱们当初应用 StructuredTaskScope 来实现同样的后果,并将子工作的生命周期限度在词法的作用域内,在本例中,也就是 try-with-resources 语句体内。这段代码更易读,而且用意也很分明。StructuredTaskScope 还主动确保以下行为:

  • 基于短路的错误处理:如果 updateInventory() 或 updateOrder() 失败,另一个将被勾销,除非它曾经实现。这是由 ShutdownOnFailure() 实现的勾销策略来治理的,咱们还能够应用其余策略。

  • 勾销流传:如果运行 handleOrder() 的线程在调用 join() 之前或调用过程中被中断的话,当该线程退出作用域时,两个分支(fork)都会被主动勾销。
  • 可察看性:线程转储文件将分明地显示工作档次,运行 updateInventory() 和 updateOrder() 的线程被显示为作用域的子线程。

Loom 我的项目情况

Loom 我的项目开始于 2017 年,经验了许多变动和提议。虚构线程最后被称为 fibers,但起初为了防止混同而从新进行了命名。现在随着 Java 19 的公布,该我的项目曾经交付了上文探讨的两个性能。其中一个是预览状态,另一个是孵化状态。因而,这些个性的稳定化之路应该会更加清晰。

这对一般的 Java 开发人员意味着什么?

当这些个性生产环境就绪时,应该不会对一般的 Java 开发人员产生太大的影响,因为这些开发人员可能正在应用某些库来解决并发的场景。然而,在一些比拟常见的场景中,比方你可能进行了大量的多线程操作然而没有应用库,那么这些个性就是很有价值的了。虚构线程能够毫不费力地代替你当初应用的线程池。依据现有的基准测试,在大多数状况下它们都能进步性能和可扩展性。结构化并发有助于简化多线程或并行处理,使其能加强壮,更易于保护。

这对 Java 库开发人员意味着什么?

当这些个性生产环境就绪时,对于应用线程或并行的库和框架来说,将是一件小事。库作者可能实现微小的性能和可扩展性晋升,同时简化代码库,使其更易保护。大多数应用线程池和平台线程的 Java 我的项目都可能从切换至虚构线程的过程中受害,候选我的项目包含 Tomcat、Undertow 和 Netty 这样的 Java 服务器软件,以及 Spring 和 Micronaut 这样的 Web 框架。我预计大多数 Java web 技术都将从线程池迁徙到虚构线程。Java web 技术和新兴的反应式编程库,如 RxJava 和 Akka,也能够无效地应用结构化并发。但这并不意味着虚构线程将成为所有问题的解决方案,异步和反应式编程依然有其实用场景和收益。

理解更多对于 Java、多线程和 Loom 我的项目的信息:

  • On the Performance of User-Mode Threads and Coroutines
  • State of Loom
  • Project Loom: Modern Scalable Concurrency for the Java Platform
  • Thinking About Massive Throughput? Meet Virtual Threads!
  • Does Java 18 finally have a better alternative to JNI?

  • OAuth for Java Developers
  • Cloud Native Java Microservices with JHipster and Istio

正文完
 0