Java 19

created onOctober 6, 2022

general availability on 20 March 2018

JEPs

new features

JEP state summary /remark
JEP 405: Record Patterns preview introduces record patterns to deconstruct record values. Record patterns and type patterns can be nested to for a declarative and composable form of data navigation and processing.
JEP 422: Linux/RISC-V Port standard port the JDK to Linux/RISC-V.
JEP 424: Foreign Function & Memory API preview eases the access for devs to code and data on the same machine as the JVM but outside of the JVM, without the drawbacks of JNI.
JEP 425: Virtual Threads preview introduces lightweight threads called virtual threads to the Java platform.
JEP 426: Vector API fourth incubator provides a second iteration of an incubator module, jdk.incubator.vector, to express vector computations that reliably compile at runtime to optimal vector hardware instructions on supported CPU architectures and thus achieve superior performance to equivalent scalar computations.
JEP 427: Pattern Matching for switch third preview introduces pattern matching for switch expressions and statements, along with extensions to the language of patterns. See comments on JEP 406: Pattern Matching for switch (Preview).
JEP 428: Structured Concurrency incubator simplifies multithreaded programming by introducing an API for structured concurrency. Structured concurrency treats multiple tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability.

JDK internal

none

deprecated

none

removed

none

some feature details

JEP 405: Record Patterns (Preview)

In JDK 16, the instanceof operator is extended to take a type pattern and perform pattern matching (see also JEP 394: Pattern Matching for instanceof and comments on ‘JEP 305: Pattern Matching for instanceof (Preview)').

Records, available since JDK 16, are transparent data containers. Record patterns are an extension of pattern matching for instanceof to match records:

record Point(int x, int y) {} void printSum(Object o) { if (o instanceof Point(int x, int y)) { System.out.println(x+y); } }

Here, is a record pattern.

pattern matching scales to match more complicated object graphs. I.e., for the following declarations:

record Point(int x, int y) {} enum Color { RED, GREEN, BLUE } record ColoredPoint(Point p, Color c) {} record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

the color of the upper left point can be extracted (and printed on System.out) with:

static void printColorOfUpperLeftPoint(Rectangle r) { if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) { System.out.println(c); } }

record patterns and generics

Record patterns for generic record classes must use the generic type. I.e. for the following declaration:

record Box<T>(T t) {}

the following two methods are correct:

static void test1(Box<Object> obj) { if (obj instanceof Box<Object>(String s)) { System.out.println("String " + s); } } static void test2(Box<Object> obj) { if (obj instanceof Box<String>(var s)) { System.out.println("String " + s); } }

whereas The following two methods are not correct and will result in compile-time errors:

static void erroneousTest1(Box<Object> bo) { if (bo instanceof Box(var s)) { // Error System.out.println("I'm a box"); } } static void erroneousTest2(Box b) { if (b instanceof Box(var t)) { // Error System.out.println("I'm a box"); } }

record patterns and switch

Since both switch expressions and pattern switch statements must both be exhaustive, the switch block must have clauses that deal with all possible values of the selector expression. For pattern labels this is determined by analysis of the types of the patterns; i.e., the case label case Fruit f matches values of type Fruit and all possible subtypes of Fruit.

I.e. for the following two class and the record declarations:

class A {} class B extends A {} record Pair<T>(T x, T y) {}

and a record Pair<A> p1 of the type record Pair<T>(T x, T y) {}, the following switch not exhaustive, because it has no match for a Pair containing two types of A:

switch (p1) { // Error! case Pair<A>(A a, B b) -> ... case Pair<A>(B b, A a) -> ... }

For the following declaration of an interface and two classes implementing the interface and the declaration of a record Pair<I> p2;:

sealed interface I permits C, D {} final class C implements I {} final class D implements I {} record Pair<T>(T x, T y) {}

this switch statement is valid, because I is sealed, so C and D cover all possible instances:

switch (p2) { case Pair<I>(I i, C c) -> ... case Pair<I>(I i, D d) -> ... }

virtual threads

platform threads vs virtual threads

Traditionally, Java has used the java.lang.Thread platform threads. A platform thread is a thin wrapper around an OS thread and occupies an OS thread for the entire lifetime of a thread. Or, in other terms, it monopolizes an OS thread. However, OS threads are limited and a large number of platform thrads can exhaust the available OS threads.

The concept of virtual threads is similar to the concept of virtual memory. A virtual thread doesn’t monopolize an underlaying OS thread, instead there is an M:N relation, where a large number of virtual threads run on a much smaller number of OS threads. A virtual thread occupies an OS thread only while it performs calculation on the CPU.

thread-per-request vs asynchronous programming style

The relatively scarce number of available OS threads eventually lead devs to abandon the thread-per-request programming style in favour of the asynchronous style in cases where a large number (i.e. in a magnitude of some thousands) of requests would run simultaneously.

In the asynchronous programming style, instead of handling a request in one thread from start to finish, the requests utilize threads from a thread pool. The pooled threads are occupied when performing calculation on the CPU and released back to the pool when waiting for I/O operations to complete.

Necessarily, here the requests have to be broken up into small units of code. In the asynchronous style, I/O operations do not wait for completion and then return the result but signal their completion to a callback. The logic for handling the request is broken up into small units, often written as lambdas. Those chunks of code are composed into a sequential pipeline. This is where the reactive frameworks come from. This programming style comes with a set of problems:

  • it is hard to use basic control features like loops and try-catch blocks.
  • because each stage of a request runs on different threads and each threads run stages belonging to different requests, debugging is hard. Stack traces provide no usable context, debuggers can’t step through the request logic.
  • profiling becomes hard too, the cost of an operation cannot be assóciated with its caller.

Virtual threads have been introduced to allow applications to scale while using the thread-per-request style.

using virtual threads

In the following example, 10k virtual threads are created. The task executed in each thread is to sleep for second. The ExecutorService creates and submits the tasks and then waits for all of them to complete. The JDK will run the code on a small number of OS threads, possibly on only one thread.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits

thread-locals

Thread-locals can be used in virtual threads the same way as they are used in plattform threads. However, care should be taken when using thread-locals in virtual threads because of the possibly vast number of virtual threads.

observing virtual threads

You can use JDK debuggers and the JDK Flight Recorder on virtual threads the same way as you can use them on platform threads.

Thread dumps are another story. The JDK’s traditional thread dump tool jstack and jcmd present threads in a flat list. This is suitable up to say a few hundred of threads, but not for thousands let alone millions of threads.

For this reason, a new kind of thread dump is introduced in jcmd to present virtual threads alongside platform threads, grouped in a meaningful way. jcmd supports the output of thread dumps in JSON format, which eases analyzation and visualization by suitable tools:

jcmd <pid> Thread.dump_to_file -format=json <file>

There are some differences in thread dumps for virtual threads as opposed to platform threads:

  • the new thread dump format lists virtual threads that are blocked in network I/O operations and virtual threads that are created by the new-thread-per-task ExecutorService.
  • object addresses, locks, JNI statistics, heap statistics, and other information that appears in traditional thread dumps is not included in the thread dump.
  • generating a new thread dump does not pause the application. This would not be feasable because of the possibly huge number of threads.

Relationships among threads can be shown if programs use structured concurrency, which is also introduced in JDK 19.

Since virtual threads are not tied to any OS thread like platform threads, virtual threads are invisible from the OS perspective.

structured concurrency

Imagine a server application that serves incoming requests and returns a (quite simple) response. Here is the single threaded version:

Response handle() throws IOException { String s = gatString(); Integer i = getInt(); return new Response(s, i); } String getString throws IOException { // do some stuff and return result as a String } Integer getInt throws IOException { // do some stuff and return result as an Integer }

There are few thing worth noting:

  • getB() is not started until getA() has successfully finished.
  • if either getA() or getB() fail (failing here means throwing an exception), the handle method fails as well. If getString() fails, getInteger() isn’t even started.

Obviously, it is desirable to have both tasks getString() and getInt() running simultaneously to utilize computing power as effective as possible and reduce the time for handle() to complete the request. Here is the (somewhat simplicistic) multithreaded version:

Response handle() throws ExecutionException, InterruptedException { Future<String> s = esvc.submit(() -> getSring()); Future<Integer> i = esvc.submit(() -> getInt()); String rs = s.get(); // join getString() Integer ri = i.get(); // join getInteger() return new Response(rs, ri); }

Again, there are a few things here worth noting:

  • if getSring() fails, handle fails as well, but getInt() will continue to run in it’s own thread. This is called a thread leak.
  • if handle() is interrupted, both getString() and getInteger() keep on running in their threads. Now we have two leaked threads.
  • if getString() takes a long while to execute and getInteger() fails in the meantime, handle will receive the exception from getInteger() only after calling i.get(). But before that happens, handle() will block at s.get() until s.get() finished, unnecessarily wasting time.

So we have (at least) three problems here:

  • no cancellation propagation where it is appropriate, but leaked threads instead.
  • no error handling with short-circuiting. If one task fails, the other task should be cancelled immediately, but that doesn’t happen here.
  • one doesn’t get what the code really does just by reading the code, but only after thinking through the possibilities what happens when?. The code doesn’t really reflect the intent of the dev.

To avoid these problems one has to manage the lifecycle of the threads for the tasks with try-with-resources and try-finally, but this is tricky. Both of which is due to the fact that the ExecutorService and Future allow unrestricted patterns of concurrency.

Here’s the handle() method using the StructuredTaskScope from structured concurrency:

Response handle() throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> s = scope.fork(() -> findUser()); Future<Integer> i = scope.fork(() -> fetchOrder()); scope.join(); // join both forks scope.throwIfFailed(); // ... and propagate errors // here, both forks have succeeded return new Response(s.resultNow(), i.resultNow()); } }

Here, we have the following features:

  • error handling with short-circuiting: scope.join() shuts down the scope on the first exception from any task because of the policy StructuredTaskScope.ShutdownOnFailure, which means that no new threads can be started for the scope and all threads within the scope join immediately. No more leaked threads.
  • cancellation propagation: if the thread running handle() is interrupted before or during scope.join(), both forks/tasks are cancelled automatically when the thread exits the scope.
  • error propagation in a simple manner at scope.throwIfFailed()
  • observability: a thread dump, generated with jcmd <pid> Thread.dump_to_file -format=json <file> displays the task hierarchy, with the threads running getString() and getInteger() shown as children of the scope.

reference

OpenJDK JDK 19 Feature List and Schedule

JEP 405: Record Patterns

JEP 425: Virtual Threads

JEP 428: Structured Concurrency