Java 19: what’s new ?

Java 19: what’s new ?

Now that Java 19 is features complete (Rampdown Phase One at the day of writing), it’s time to walk throught all the functionalities that brings to us, developers, this new version.

This article is part of a series on what’s new on the last versions of Java, for those who wants to read the others, here are the links: Java 18, Java 17, Java 16, Java 15, Java 14, Java 13, Java 12, Java 11Java 10, and Java 9.

JEP 405 : Record Patterns (Preview)

This JEP aims at enriching the Java pattern matching with record patterns that allow to deconstruct a record into its attributes.

A record is a new type whose purpose is to be a data container, it was introduced in Java 14.

Before JEP 405, if you wanted pattern match on a record and then access its attributes, you would have written code like this:

record Point(int x, int y) {}

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

JEP 405 allows deconstructing the record and directly accessing its attributes when the pattern matches, which simplifies the code as follows:

record Point(int x, int y) {}

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

Instead, in the code block executed when the pattern match, to have access to the variable Point p; you have direct access to the int x, int y attributes of the Point record.

This is the first step in type deconstruction in Java, we can hope to see deconstruction soon arrive for all classes and not just records.

More information in the JEP 405.

JEP 422 : Linux/RISC-V Port

RISC-V is a free and open source Reduced Instruction Set Computer (RISC) Instruction Set Architecture (ISA) originally designed at the University of California, Berkeley, and now collaboratively developed under the RISC-V International sponsorship: https://riscv.org.

RISC-V defines several ISAs, only the RV64GV (general purpose 64bit) instruction set has been ported.

This port includes support for JIT (C1 and C2) as well as all existing GCs (including ZGZ and Shenandoah).

More information in the JEP 422.

JEP 425 : Virtual Threads (Preview)

Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.

The JEP 425 introduces in preview feature Virtual Threads (sometimes also called green threads or lightweight threads), these are lightweight threads, with low creation and scheduling cost, which facilitate the writing of concurrent applications.

This is OpenJDK’s Loom project.

Classic Java threads are implemented via OS threads, what’s wrong with this thread implementation?

  • Creating a thread in Java involves creating an OS thread and therefore a system call, which is expensive.
  • A thread has a fixed stack size, default 1MB on amd64 Linux, configurable via -Xss or -XX:ThreadStackSize.
  • A thread will be scheduled via the OS scheduler, each change in the execution of a thread will therefore cause a context switch which is a costly operation.

To overcome these problems, concurrent applications have used several types of construct:

  • Thread pools which allow to reuse a thread for several requests (HTTP, JDBC, …). The degree of concurrency of the application is the size of the pool.
  • Reactive programming which will open a very small number of threads (1 or 2 per CPU unit), and rely on an event loop mechanism to process a set of concurrent requests on these few threads.

But these constructs imply complexity when developing an application. The idea of Virtual Threads is to offer threads that are inexpensive to create, and therefore to make it possible to avoid using a pool of threads or an event loop and to use one thread for each request. This is the promise of the Loom project.

The principle is to offer virtual threads which are created and scheduled by the JVM, and use a pool of OS threads, which are called carrier threads. The JVM will then automatically mount and unmount the virtual threads of the carrier threads when necessary (during I/O for example). The stacks of the threads will be stored in the heap of the JVM, their size not being fixed any more, a gain in memory is possible.

The easiest way to create virtual threads is via Executors.newVirtualThreadPerTaskExecutor(), then we have an ExecutorService which will execute each tasks in a new virtual thread.

Where virtual threads are most interesting is for applications that require high concurrency (several tens of thousands of threads) and/or that are not CPU bound (generally those performing I/O). Be careful, using virtual threads for CPU bound applications, applications making intense calculations for example, is counter-productive, because having more threads than CPU for this type of application negatively impacts performance.

There are limitations in the current implementation of virtual threads, in some cases a virtual thread will pin the carrier thread which will not be able to process other virtual threads:

  • The use of the keyword synchronized
  • Using a native method or the Foreign Function API of the JEP 424

Because of these limitations, virtual threads are not necessarily the solution to all concurrency problems, although improvements may be made in future versions of Java. It will also be necessary for the ecosystem to become compatible with virtual threads (for example by avoiding the use of synchronized blocks).

More information in the JEP 425.

JEP 428 : Structured Concurrency (Incubator)

Simplify multithreaded programming by introducing a library 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.

Incubator module which offers a new API to simplify writing multi-threaded code by allowing several concurrent tasks to be treated as a single processing unit.

Even if Virtual Threads are the stars of this new version of Java, the Structured Concurrency API is just as interesting for me, if not more, at least from a developer perspective, because it will strongly impact the way of writing multi-threaded / concurrent code.

The goal of this API is to be able to write a more readable multi-threaded code and with less risk of errors in its implementation via better error handling.

Before the Structured Concurrency API, to run a set of concurrent tasks and then join the results; we wrote code like this (example taken from the JEP).

ExecutorService ex = Executors.newFixedThreadPool(nbCores);
Response handle() throws ExecutionException, InterruptedException {
    Future<String>  user  = es.submit(() -> findUser());
    Future<Integer> order = es.submit(() -> fetchOrder());
    String theUser  = user.get();   // Join findUser 
    int    theOrder = order.get();  // Join fetchOrder
    return new Response(theUser, theOrder);
}

There are several issues here:

  • If an error occurs in the fetchOrder() method, we will still wait for the end of the findUser() task
  • If an error occurs in the findUser() method, then the handle() method will terminate, but the thread executing fetchOrder()< /code> will continue to run, it's a thread leak.
  • If the handle() method is interrupted, this interruption is not propagated to the subtasks and their threads will continue to execute, this is a thread leak.

With the Structured Concurrency API, you can use the StructuredTaskScope class which allows you to launch a set of tasks and manage them as a single processing unit. The principle is to split a task into sub-tasks (via fork()) which will end in the same place: scope.join(). Here is an example taken from the JEP.

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String>  user  = scope.fork(() -> findUser()); 
        Future<Integer> order = scope.fork(() -> fetchOrder());

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

        // Here, both forks have succeeded, so compose their results
        return new Response(user.resultNow(), order.resultNow());
    }
}

Here the two subtasks findUser() and fetchOrder() are started in their own thread (by default a virtual thread), but are joined and potentially canceled together (like a processing unit). Their exceptions and results will be aggregated into the parent task, and any interrupts from the parent thread will be propagated to the child threads. During a thread dump, we will see the subtasks as children of the parent thread, making it easier to debug code written in this way.

More information in the JEP 428.

Features that remain in preview

The following features remain in preview (or in incubator module).

For details on these, you can refer to my previous articles.

  • JEP-426 – Vector API: fourth feature incubation. This new version integrates with the Foreign Function & Memory API by allowing to read/write a vector from a MemorySegments.
  • JEP-424 – Foreign Function & Memory API: after two incubations for these two functionalities combined in the same JEP, these move to a preview feature.
  • JEP-427 – Pattern Matching for switch: third preview with the introduction of when clause for guards instead of &&.

Miscellaneous

Various JDK additions:

  • BigInteger.parallelMultiply(): same as BigInteger.parallelMultiply() but will use a parallel algorithm which will use more CPU and memory resources for large numbers.< /li>
  • Integer and Long see the addition of the compress and expand methods
  • HashMap.newHashMap(int), HashSet.newHashSet(int), LinkedHashMap.newLinkedHashMap(int) and LinkedHashSet. newLinkedHashSet(int): these methods allow you to create maps or sets that can accommodate the number of elements passed as a parameter without triggering a resize.
  • The constructors of the Locale class have been deprecated, you must now construct a locale via Locale.of().

All new JDK 19 APIs can be found in The Java Version Almanac – New APIs in Java 19.

Internal changes, performance, and security

Each new version of the JDK brings its performance optimizations (including GC and intrisics methods), and security improvements.

On the security side, the focus has been on strengthening JVM security and TLS performance. You can refer to Sean Mullan’s article for an exhaustive list of security enhancements included in this release: JDK 19 Security Enhancements.

Thomas Schatzl describes the changes on the Garbage Collectors side included in this release in his article JDK 19 G1/Parallel/Serial GC changes.

Conclusion

The long-awaited JEP 425 Virtual Threads has finally arrived and could be a game-changer in concurrent programming by lowering the cost of creating threads in Java. In conjunction with the JEP 428 which makes it easier to write concurrent code and with better error management, a mini revolution is comming ;).

Of course, adoption of virtual threads will be slow, as the ecosystem will need to be updated to support them, but it’s a big step for Java and the JVM.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.