Java 18, 19 & 20: Virtual Threads on the Horizon
Java 18, 19, and 20 were bridge releases between the Java 17 and Java 21 LTS versions. They introduced UTF-8 as the default charset, a simple built-in web server, and — most significantly — previewed virtual threads, structured concurrency, and record patterns that would be finalised in Java 21.
Java Evolution — Part 8
Java 18 (March 22, 2022), Java 19 (September 20, 2022), and Java 20 (March 21, 2023) were non-LTS bridge releases between the Java 17 and Java 21 LTS versions. Their most important role was incubating and previewing the Project Loom features — virtual threads and structured concurrency — that would be finalised in Java 21.
UTF-8 by Default (Java 18)
JEP 400 made UTF-8 the default charset for the Java standard APIs. Before Java 18, the default charset was platform-dependent — UTF-8 on Linux and macOS, but Windows-1252 (or another Windows code page) on Windows. This caused subtle, hard-to-reproduce bugs when code was developed on one OS and deployed on another.
// Before Java 18 — charset depended on the OSFileWriter writer = new FileWriter("output.txt"); // platform charset!FileReader reader = new FileReader("input.txt"); // platform charset!
// Java 18+ — always UTF-8 by defaultFileWriter writer = new FileWriter("output.txt"); // UTF-8FileReader reader = new FileReader("input.txt"); // UTF-8
// Explicit charset still works and is recommended for clarityFileWriter writer = new FileWriter("output.txt", StandardCharsets.UTF_8);This change is a correctness improvement for the vast majority of modern applications. If your application explicitly relied on a non-UTF-8 platform charset, you can restore the old behaviour with -Dfile.encoding=COMPAT.
Simple Web Server (Java 18)
JEP 408 added a minimal HTTP file server to the JDK, launchable from the command line. It is designed for prototyping, testing, and quick demonstrations — not production use.
# Serve the current directory on port 8000jwebserver
# Specify a directory and portjwebserver -p 9090 -d /path/to/files
# Output# Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".# Serving /path/to/files and subdirectories on 127.0.0.1 port 9090# URL http://127.0.0.1:9090/You can also create a simple file server programmatically:
var server = SimpleFileServer.createFileServer( new InetSocketAddress(8080), Path.of("/var/www/html"), SimpleFileServer.OutputLevel.VERBOSE);server.start();Code Snippets in Javadoc (Java 18)
JEP 413 introduced the @snippet tag for Javadoc, replacing the old <pre>{@code ...}</pre> pattern with a dedicated, IDE-friendly syntax that supports syntax highlighting and external file references.
/** * Demonstrates the use of {@code Stream.toList()}. * * {@snippet : * List<String> names = Stream.of("Alice", "Bob", "Charlie") * .filter(s -> s.length() > 3) * .toList(); // @highlight substring="toList" * } */public void example() {}Virtual Threads (Preview — Java 19 & 20)
Project Loom is the most significant concurrency improvement in Java’s history. Its centrepiece is virtual threads — lightweight threads managed by the JVM rather than the OS.
The Problem with Platform Threads
Traditional Java threads (platform threads) map 1:1 to OS threads. OS threads are expensive:
- Each consumes approximately 1–2 MB of stack memory
- Context switching between OS threads is slow
- A typical server can sustain only a few thousand concurrent platform threads
This forces developers to use asynchronous, non-blocking programming models (reactive, callbacks, CompletableFuture) to achieve high concurrency — code that is notoriously difficult to write, read, and debug.
Virtual Threads
Virtual threads are managed by the JVM. They are extremely lightweight — millions can exist simultaneously — and they are mounted onto a small pool of carrier (platform) threads. When a virtual thread blocks (e.g., waiting for I/O), it is unmounted from its carrier thread, which is then free to run another virtual thread.
// Java 19 preview — creating virtual threadsThread vt = Thread.ofVirtual().start(() -> { System.out.println("Running in virtual thread: " + Thread.currentThread());});
// Using the Executors factorytry (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 1_000_000; i++) { executor.submit(() -> { // Each task runs in its own virtual thread Thread.sleep(Duration.ofMillis(100)); return "done"; }); }} // executor.close() waits for all tasks to completeThe Key Insight
Virtual threads allow you to write simple, synchronous, blocking code and get the scalability of asynchronous code. You do not need to change your programming model — just use virtual threads instead of platform threads.
// Simple blocking code that scales to millions of concurrent requests// with virtual threads — no callbacks, no reactive chainsvoid handleRequest(HttpExchange exchange) throws IOException { // This blocks the virtual thread, not the OS thread String data = fetchFromDatabase(exchange.getRequestURI()); String processed = callExternalApi(data); exchange.sendResponseHeaders(200, processed.length()); exchange.getResponseBody().write(processed.getBytes());}Virtual threads were finalised in Java 21.
Structured Concurrency (Incubator — Java 19 & 20)
Structured Concurrency (JEP 428/437) is a companion to virtual threads. It treats a group of related concurrent tasks as a single unit of work, with a well-defined lifecycle.
The core idea: if a task spawns subtasks, all subtasks must complete (or be cancelled) before the spawning task completes. This mirrors the structure of sequential code and makes concurrent code easier to reason about.
// Java 19 incubator — structured concurrencyResponse handle(Request request) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<UserInfo> userFuture = scope.fork(() -> fetchUser(request.userId())); Future<OrderList> ordersFuture = scope.fork(() -> fetchOrders(request.userId())); Future<Preferences> prefsFuture = scope.fork(() -> fetchPreferences(request.userId()));
scope.join(); // wait for all three scope.throwIfFailed(); // propagate any failure
return new Response( userFuture.resultNow(), ordersFuture.resultNow(), prefsFuture.resultNow() ); }}With ShutdownOnFailure, if any subtask fails, the scope cancels the remaining subtasks immediately. This prevents resource leaks and avoids waiting for tasks whose results are no longer needed.
Structured Concurrency was finalised in Java 25.
Record Patterns (Preview — Java 19 & 20)
JEP 405/432 extended pattern matching to support record patterns — deconstructing record components directly in instanceof and switch.
record Point(int x, int y) {}record ColoredPoint(Point point, Color color) {}
// Java 19 preview — record pattern in instanceofObject obj = new ColoredPoint(new Point(1, 2), Color.RED);
if (obj instanceof ColoredPoint(Point(int x, int y), Color c)) { System.out.printf("x=%d, y=%d, color=%s%n", x, y, c);}
// Record patterns in switchString describe(Object o) { return switch (o) { case Point(int x, int y) -> "Point at (%d,%d)".formatted(x, y); case ColoredPoint(Point(int x, int y), var c) -> "Colored point at (%d,%d)".formatted(x, y); default -> "Unknown"; };}Record patterns were finalised in Java 21.
Pattern Matching for switch — Continued Refinement
Java 19 and 20 continued refining pattern matching for switch with guarded patterns using the when keyword:
// Java 20 preview — guarded patternsString classify(Object obj) { return switch (obj) { case Integer i when i < 0 -> "negative integer"; case Integer i when i == 0 -> "zero"; case Integer i -> "positive integer"; case String s when s.isBlank() -> "blank string"; case String s -> "non-blank string: " + s; default -> "other"; };}Summary
Java 18–20 were deliberately focused on incubating the Project Loom features. The virtual threads preview in Java 19 was arguably the most anticipated Java feature in a decade.
| Feature | Version | Key Benefit |
|---|---|---|
| UTF-8 by Default | 18 | Consistent charset across all platforms |
| Simple Web Server | 18 | Quick file serving for prototyping |
@snippet in Javadoc | 18 | Better code examples in documentation |
| Virtual Threads (Preview) | 19 & 20 | Millions of lightweight threads; simple blocking code |
| Structured Concurrency (Incubator) | 19 & 20 | Safe, leak-free concurrent task management |
| Record Patterns (Preview) | 19 & 20 | Deconstruct records in patterns |
Guarded Patterns (when) | 20 | Conditional case arms in switch |