我的学习笔记

土猛的员外

Java21新特性——ZGC、虚拟线程和结构化并发

前两天同事和我说现在可以回来看看Java了,Java17可能更新的还不多,但是Java21这次释放了一大波新特性,会是接下来五六年的一个新起点,至少这次Java21支持到2026年9月。于是我抽了点时间看了一下Java21,确实有很多新特性,总结其中几个,做个收藏。

下面就先记录三种主要特性吧:

  • 新的垃圾收集器——Generational ZGC;
  • Java的“协程”——Virtual Threads;
  • 结构化并发——Structured Concurrency。

Garbage Collection – Level Up Coding

一.Generational ZGC:新的垃圾收集器

垃圾收集”的概念本质上是关于自动内存管理的。每次创建新的数据结构时,运行时都需要内存来完成。如果我们不再需要一个对象或留下一个堆栈框架,并且它的变量超出了作用域,就必须有人清理相关的内存,即“垃圾”,否则我们将很快了解Java的OutOfMemoryError

垃圾收集(GC)的缺点是需要时间来清理和重新安排内存,这会引入了一定的运行时开销,因为GC运行的实际发生时间点通常是不确定的,并且不是手动触发的。特别是在多线程上扩展的高吞吐量、大型内存消耗的应用程序可能会遭受长时间的“GC暂停”,GC暂停的另外一个更可怕的名字是“世界停止”。但是Java之所以能和C/C++抢夺市场,很重要的一点就是Java程序员不需要手动去回收变量的内存占用,GC在这一方面贡献巨大。

1.如何选择GC

GC算法主要关注三个指标:

  • 吞吐量:在长时间内未使用GC的总时间的百分比。
  • 延迟:应用程序的整体响应性,受GC暂停的影响。
  • Footprint:应用对内存的需求,比如内存使用100M/s、GC速度80M/s,10s内存进行一次回收,那内存剩下的200M称为峰值。

与许多问题一样,你不可能针对所有问题进行优化,所以每个GC都需要在它们之间找到一个平衡。以下是一些场景及其匹配GC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 垃圾回收器 | 使用场景
====================|==========================================
* Serial | * 小数据集 (最大~100 MB max)
| * 有限的资源 (e.g., single core)
| * 低暂停时间
--------------------|------------------------------------------
* Parallel | * 多核系统的峰值性能
| * 高计算负载
| * 大于1秒的暂停是可以接受的
--------------------|------------------------------------------
* G1 | * 响应时间超过吞吐量
* CMS | * 大堆heap
| * 暂停时间小于1秒
--------------------|------------------------------------------
* Shenandoah | * 最小化暂停时间
| * 可预测的延迟
--------------------|------------------------------------------
* ZGC | * 响应时间是高优先级的,和/或
| * 非常大的堆heap
--------------------|------------------------------------------
* Epsilon GC | * 性能测试和故障排除

每种方法都有自己的优缺点,这在很大程度上取决于应用程序的需求和可用资源。

Generational ZGC在表里面的选择情况已经说的比较清楚了。

2.世代特点

“世代假设”是一种观察结果,即年轻的对象比老的对象更有可能“消失”,至少在大多数情况下是这样。这就是为什么根据对象的年龄不同处理它们是有益的。改进年轻对象的收集需要更少的资源并产生更多的内存。

即使不按世代处理,ZGC在GC暂停时间上也有很大的改进,至少在有足够的可用资源的情况下,它回收内存的速度比并发线程消耗内存的速度要快。但是,所有对象都将被存储,而不考虑它们的年龄,并且在GC运行时必须检查所有对象。

在Java 21中,ZGC将堆划分为两个逻辑代:一个用于最近分配的对象,另一个用于分配长期对象。GC可以专注于更频繁地收集更年轻、更有希望的对象,而无需增加暂停时间,将它们保持在1毫秒以下。

分代ZGC可以带来的好处:

  • 减低分配失速的风险
  • 降低所需的堆开销
  • 降低GC对CPU的影响

与不分代的ZGC相比,所有这些优点都不会显著降低吞吐量。此外,不需要自己配置代的大小、使用的线程或年龄限制。

3.如何使用

在典型的Java方式中,新的ZGC不会在可用时立即强加给我们。相反,它将与它的非代际前身一起提供。你可以用java 参数来配置:

1
2
3
4
5
# Enable ZGC (defaults to non-generational)
$ java -XX:+UseZGC

# Use Generational ZGC
$ java -XX:+UseZGC -XX:+ZGenerational

请注意,一代ZGC应该随着时间的推移取代它的前身,并成为默认的ZGC。在这一点上,你可以用拮抗参数来关闭它,通过将+(加)替换为-(减/破折号):

1
2
# Do not use Generational ZGC
$ java -XX:+UseZGC -XX:-ZGenerational

它还计划在以后的版本中完全删除非分代ZGC。

Java Virtual Threads — Easy introduction | by Ram Lakshmanan | Medium

2.Virtual Threads——Java自己的“协程”

Java的多线程并发编程一直是比较难学的,像我这样的老程序员,在设计多线程的时候,还要自己“心算”一下会不会出问题,每次都需要非常谨慎。但后来GoLang的协程出现,你会非常羡慕为什么人家可以用的如此丝滑。这不,Java自己的“协程”也来了,它就是虚拟线程——Virtual Threads。

其实虚拟线程在JDK19或JDK20中就开始出现,只是在Java21中变得更加成熟和自然。

1.虚拟线程和协程的异同点

虚拟线程是基于协程的线程,与其他语言中的协程有相似之处,但也有一些不同之处。虚拟线程附属于主线程,如果主线程被销毁,虚拟线程将不再存在。

以下是一些相似点与区别:

相似点:

  • 虚拟线程(Virtual Threads)和协程都是轻量级的,它们的创建和销毁开销比传统的操作系统线程要小。
  • Virtual Threads和协程都可以通过挂起和恢复在线程之间切换,从而避免线程上下文切换的开销。
  • Virtual Threads和协程都可以异步和非阻塞的方式处理任务,提高应用程序的性能和响应能力。

区别:

  • Virtual Threads在JVM级别实现,而协程在语言级别实现。因此,虚拟线程的实现可以与任何支持JVM的语言一起使用,而协程的实现需要特定的编程语言支持。
  • Virtual Threads是基于线程的协程实现,所以他们可以使用线程相关的api,如ThreadLocalLockSemaphore。协程不依赖于线程,通常需要特定的异步编程框架和api。
  • Virtual Threads的调度由JVM管理,协程的调度由编程语言或异步编程框架管理。因此,Virtual Threads可以更好地与其他线程协作,而协程则更适合处理异步任务。

2.虚拟线程的优势

一般来说,虚拟线程是一种新的线程类型,它可以提高应用程序的性能和资源利用率,同时还可以使用传统的线程相关api。虚拟线程与协程有许多相似之处,但也有一些不同之处。

Virtual Threads确实可以使多线程编程更容易、更高效。与传统的操作系统线程相比,创建和销毁虚拟线程的开销更小,线程上下文切换的开销也更小,因此可以大大降低多线程编程中的资源消耗和性能瓶颈。

使用Virtual Threads,开发人员可以像编写传统线程代码一样编写代码,而不必担心线程的数量和调度,因为JVM将自动管理虚拟线程的数量和调度。此外,虚拟线程还支持传统的线程相关api,如ThreadLocalLockSemaphore,这使得开发人员更容易将传统线程代码迁移到虚拟线程中。

虚拟线程的引入使多线程编程更高效、更简单、更安全,允许开发人员更多地关注业务逻辑,而不必过多关注底层线程管理。

3.如何使用虚拟线程

首先,声明一个线程类,实现from Runnable,并实现run方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleThread implements Runnable{

@Override
public void run() {
System.out.println("name:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

然后,您可以使用这个线程类并启动线程。

1
2
Thread thread = new Thread(new SimpleThread());
thread.start();

拥有虚拟线程后,如何实现它?

1
Thread.ofPlatform().name("thread-test").start(new SimpleThread());

下面是使用虚拟线程的几种方法。

1. 直接启动一个虚拟线程

1
Thread thread = Thread.startVirtualThread(new SimpleThread());

2. 使用ofVirtual(),构建器模式启动虚拟线程,您可以设置线程名称,优先级,异常处理和其他配置

1
2
3
4
5
6
7
8
9
10
11
Thread.ofVirtual()
.name("thread-test")
.start(new SimpleThread());
// Or
Thread thread = Thread.ofVirtual()
.name("thread-test")
.uncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + e.getMessage());
})
.unstarted(new SimpleThread());
thread.start();

3. 使用Factory创建线程

1
2
3
4
ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(new SimpleThread());
thread.setName("thread-test");
thread.start();

4. 使用Executor

1
2
3
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
Future<?> submit = executorService.submit(new SimpleThread());
Object o = submit.get();

image-20230926164502116

3.Structured Concurrency——结构化并发

结构化并发是一种编程范例,旨在通过提供结构化且易于遵循的方法来简化并发编程。使用结构化并发,开发人员可以创建更容易理解和调试的并发代码,并且更不容易出现竞争条件和其他与并发相关的错误。在结构化并发中,所有并发代码都被结构化为定义良好的工作单元,称为任务。任务以结构化的方式创建、执行和完成,并且任务的执行总是保证在其父任务完成之前完成。

1.结构化并发优势

结构化并发可以使多线程编程更容易、更可靠。在传统的多线程编程中,线程的启动、执行和终止都是由开发人员手动管理的,因此容易出现线程泄漏、死锁和异常处理不当等问题。

使用结构化并发,开发人员可以更自然地组织并发任务,使任务之间的依赖关系更清晰,代码逻辑更简洁。结构化并发还提供了一些异常处理机制,以便更好地管理并发任务中的异常,避免异常导致的程序崩溃或数据不一致。

此外,结构化并发还可以通过限制并发任务的数量和优先级来防止资源竞争和短缺。这些特性使开发人员更容易实现高效可靠的并发程序,而无需过多关注底层线程管理。

2.使用方法

想想下面的场景。假设您有三个任务要同时执行。只要其中任何一个任务完成并返回结果,就可以直接使用该结果,而其他两个任务可以停止。例如,天气服务通过三个通道获取天气条件,只要有一个通道返回它。

当然,在这种情况下,在Java 8下应该做的事情也是可能的。

1
2
3
List<Future<String>> futures = executor.invokeAll(tasks);

String result = executor.invokeAny(tasks);

使用ExecutorService的invokeAll和invokeAny实现,但是会有一些额外的工作。获得第一个结果后,需要手动关闭另一个线程。

在JDK21中,它可以通过结构化编程实现。

ShutdownOnSuccess捕获第一个结果并关闭任务范围以中断未完成的线程并唤醒调用线程。

在这种情况下,任何子任务的结果都可以直接获得,而无需等待其他未完成任务的结果。

它定义了获取第一个结果或在所有子任务失败时抛出异常的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
Future<String> res1 = scope.fork(() -> runTask(1));
Future<String> res2 = scope.fork(() -> runTask(2));
Future<String> res3 = scope.fork(() -> runTask(3));
scope.join();
System.out.println("scope:" + scope.result());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}

public static String runTask(int i) throws InterruptedException {
Thread.sleep(1000);
long l = new Random().nextLong();
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}

ShutdownOnFailure

执行多个任务,只要其中一个失败(发生异常或抛出其他活动异常),停止其他未完成的任务,并使用作用域。未能捕获并抛出异常。

如果所有任务都没问题,则使用Feature.get()*Feature.resultnow()来获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws IOException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> res1 = scope.fork(() -> runTaskWithException(1));
Future<String> res2 = scope.fork(() -> runTaskWithException(2));
Future<String> res3 = scope.fork(() -> runTaskWithException(3));
scope.join();
scope.throwIfFailed(Exception::new);

String s = res1.resultNow();
System.out.println(s);
String result = Stream.of(res1, res2,res3)
.map(Future::resultNow)
.collect(Collectors.joining());
System.out.println("result:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String runTaskWithException(int i) throws InterruptedException {
Thread.sleep(1000);
long l = new Random().nextLong(3);
if (l == 0) {
throw new InterruptedException();
}
String s = String.valueOf(l);
System.out.println(i + "task:" + s);
return s;
}

其他

Java21还有很多新特性,比如Scoped Values、String Templates、Switch Pattern Matching、Sequenced Collections、Record Patterns等,后面有时间在写,哈。


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号