Java 21: The Concurrency Revolution — LTS
Java 21 is a landmark LTS release. It finalised virtual threads (Project Loom), sequenced collections, record patterns, and pattern matching for switch — while previewing string templates and unnamed classes. It is the most feature-rich LTS release since Java 8.
Java Evolution — Part 9
Java 21, released on September 19, 2023, is the LTS release that every Java developer has been waiting for. It delivers virtual threads as a production-ready feature, finalises the pattern matching story, introduces sequenced collections, and previews string templates. It is the most significant LTS release since Java 8.
Virtual Threads — Finalised (JEP 444)
Virtual threads, previewed in Java 19 and 20, are now a stable, production-ready feature. They are the centrepiece of Project Loom and represent the most important concurrency improvement in Java’s history.
Creating Virtual Threads
// Method 1: Thread.ofVirtual()Thread vt = Thread.ofVirtual() .name("my-virtual-thread") .start(() -> System.out.println("Hello from virtual thread!"));
// Method 2: Thread factoryThreadFactory factory = Thread.ofVirtual().factory();Thread t = factory.newThread(() -> doWork());
// Method 3: Executor (recommended for most use cases)try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100_000; i++) { int taskId = i; executor.submit(() -> processTask(taskId)); }} // Waits for all tasks to completeThe Scalability Story
The following example demonstrates the scalability difference. With platform threads, creating 100,000 threads would exhaust memory. With virtual threads, it is trivial:
// This would fail with platform threads (OutOfMemoryError)// With virtual threads, it completes in secondstry (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var futures = IntStream.range(0, 100_000) .mapToObj(i -> executor.submit(() -> { Thread.sleep(Duration.ofMillis(100)); // simulate I/O return i * 2; })) .toList();
int total = futures.stream() .mapToInt(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } }) .sum();
System.out.println("Total: " + total);}Virtual Threads and Frameworks
Spring Boot 3.2+, Quarkus, Micronaut, and other frameworks support virtual threads. In Spring Boot, enabling them is a single configuration property:
spring: threads: virtual: enabled: trueWith this enabled, every request is handled by a virtual thread, giving you the scalability of reactive programming with the simplicity of synchronous code.
What Virtual Threads Are Not
Virtual threads are not a silver bullet for CPU-bound work. They excel at I/O-bound workloads — database queries, HTTP calls, file operations — where threads spend most of their time waiting. For CPU-intensive computation, parallel streams or ForkJoinPool remain the right tools.
Sequenced Collections (JEP 431)
Java 21 introduced three new interfaces to the collections framework to represent collections with a defined encounter order:
| Interface | Extends | Meaning |
|---|---|---|
SequencedCollection<E> | Collection<E> | Has a first and last element |
SequencedSet<E> | Set<E>, SequencedCollection<E> | Ordered set |
SequencedMap<K,V> | Map<K,V> | Map with ordered entries |
Before Java 21, getting the first or last element of a List or LinkedHashMap required awkward workarounds:
// Before Java 21 — getting first/last elementsList<String> list = new ArrayList<>(List.of("a", "b", "c"));
String first = list.get(0); // IndexOutOfBoundsException if emptyString last = list.get(list.size() - 1); // verbose
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();map.put("one", 1); map.put("two", 2); map.put("three", 3);
String firstKey = map.keySet().iterator().next(); // awkward// Java 21 — SequencedCollection methodsList<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"list.getLast(); // "c"list.addFirst("z"); // ["z", "a", "b", "c"]list.addLast("z"); // ["a", "b", "c", "z"]list.removeFirst(); // removes and returns first elementlist.removeLast(); // removes and returns last elementlist.reversed(); // a reversed view — no copy made
// SequencedMapLinkedHashMap<String, Integer> map = new LinkedHashMap<>();map.put("one", 1); map.put("two", 2); map.put("three", 3);
map.firstEntry(); // Map.Entry("one", 1)map.lastEntry(); // Map.Entry("three", 3)map.sequencedKeySet(); // SequencedSet view of keysmap.sequencedValues(); // SequencedCollection view of valuesmap.sequencedEntrySet(); // SequencedSet view of entriesmap.reversed(); // reversed view of the mapRecord Patterns — Finalised (JEP 440)
Record patterns, previewed in Java 19 and 20, are now stable. They allow you to deconstruct record components directly in pattern matching expressions.
record Point(int x, int y) {}record Line(Point start, Point end) {}
// Deconstruct a record in instanceofObject obj = new Line(new Point(0, 0), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); System.out.printf("Line length: %.2f%n", length); // 5.00}
// Deconstruct in switchString describe(Object o) { return switch (o) { case Point(int x, int y) when x == 0 && y == 0 -> "Origin"; case Point(int x, int y) when x == 0 -> "On Y-axis"; case Point(int x, int y) when y == 0 -> "On X-axis"; case Point(int x, int y) -> "Point(%d,%d)".formatted(x, y); case Line(Point s, Point e) -> "Line from %s to %s".formatted(s, e); default -> "Unknown"; };}Pattern Matching for switch — Finalised (JEP 441)
Pattern matching for switch, previewed since Java 17, is now a stable language feature.
// Stable in Java 21sealed interface Expr permits Num, Add, Mul {}record Num(double value) implements Expr {}record Add(Expr left, Expr right) implements Expr {}record Mul(Expr left, Expr right) implements Expr {}
double eval(Expr expr) { return switch (expr) { case Num(double v) -> v; case Add(var l, var r) -> eval(l) + eval(r); case Mul(var l, var r) -> eval(l) * eval(r); };}
// UsageExpr expression = new Add(new Num(3), new Mul(new Num(4), new Num(5)));System.out.println(eval(expression)); // 23.0The combination of sealed interfaces, records, and pattern matching for switch enables a style of programming that was previously only available in functional languages.
String Templates (Preview — JEP 430)
String templates provide safe, extensible string interpolation. Unlike simple string concatenation, they allow the interpolation to be processed by a template processor.
// Java 21 preview — STR template processorString name = "Alice";int age = 30;
String greeting = STR."Hello, \{name}! You are \{age} years old.";// "Hello, Alice! You are 30 years old."
// Expressions are evaluatedString result = STR."The sum of 2 + 2 is \{2 + 2}.";// "The sum of 2 + 2 is 4."
// Multi-line with text blocksString json = STR.""" { "name": "\{name}", "age": \{age} } """;The FMT processor supports printf-style format specifiers:
double price = 1234.567;String formatted = FMT."Price: $%,.2f\{price}";// "Price: $1,234.57"Note: String templates were removed in Java 23 and are being redesigned. The feature as described here (with
STR.andFMT.) was available only in Java 21 and 22 as a preview.
Unnamed Classes and Instance Main Methods (Preview — JEP 445)
Java 21 began simplifying the entry point for small programs. The goal is to make Java more accessible to beginners by removing the ceremony of public class, static, and String[] args.
// Java 21 preview — unnamed class, simplified mainvoid main() { System.out.println("Hello, World!");}This is a complete, runnable Java program. No class declaration, no static, no args. This feature was finalised in Java 25.
Generational ZGC (JEP 439)
Java 21 made Generational ZGC available, extending ZGC with generational garbage collection. Most objects die young — Generational ZGC exploits this by maintaining separate young and old generations, collecting the young generation more frequently. This reduces memory overhead and improves throughput compared to non-generational ZGC.
# Enable Generational ZGCjava -XX:+UseZGC -XX:+ZGenerational MyApplicationSummary
Java 21 is the most feature-rich LTS release since Java 8. It delivers on years of preview work and sets a new baseline for modern Java development.
| Feature | Key Benefit |
|---|---|
| Virtual Threads (Final) | Millions of lightweight threads; simple blocking code at scale |
| Sequenced Collections | Uniform API for first/last element access across all ordered collections |
| Record Patterns (Final) | Deconstruct records in pattern matching |
Pattern Matching for switch (Final) | Type-safe, exhaustive dispatch |
| String Templates (Preview) | Safe, extensible string interpolation |
| Unnamed Classes (Preview) | Simplified entry points for small programs |
| Generational ZGC | Lower GC overhead through generational collection |