-
Notifications
You must be signed in to change notification settings - Fork 0
Java 21
Release Date: September 19, 2023 | Type: LTS | Status: ✅ Active — the current recommended production LTS
Java 21 is the most consequential LTS since Java 8. Virtual Threads, Pattern Matching for switch, and Record Patterns all go standard simultaneously — giving Java both a modern concurrency model and a modern data-oriented programming style in one release.
mindmap
root((Java 21 LTS))
Virtual Threads
STANDARD - JEP 444
Pattern Matching switch
STANDARD - JEP 441
Record Patterns
STANDARD - JEP 440
Sequenced Collections
JEP 431
String Templates
Preview - JEP 430
Structured Concurrency
Preview - JEP 453
Scoped Values
Preview - JEP 446
Generational ZGC
JEP 439
// Simple: one virtual thread per task
Thread.ofVirtual().name("worker-1").start(() -> {
System.out.println("Running on: " + Thread.currentThread());
});
// The production pattern — virtual-thread-per-task executor
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> results = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> callDownstreamService(i)))
.toList();
for (Future<String> future : results) {
System.out.println(future.get()); // blocking .get() is fine — cheap threads!
}
}Spring Boot 3.2+ integration (real-world usage):
# application.yml
spring:
threads:
virtual:
enabled: true # Tomcat/servlet container uses virtual threads per request@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
// This blocking JDBC call is now FINE — the virtual thread
// unmounts from its carrier while waiting on the DB,
// freeing that carrier to serve other requests
return orderRepository.findById(id);
}
}flowchart TD
subgraph "Before: Platform Threads"
A1[Request 1] --> T1[Thread 1 - 1MB stack]
A2[Request 2] --> T2[Thread 2 - 1MB stack]
A3[Request N] --> T3["Thread N...\nOS thread limit ~thousands"]
end
subgraph "After: Virtual Threads"
B1[Request 1] --> V1[Virtual Thread 1]
B2[Request 2] --> V2[Virtual Thread 2]
B3[Request N] --> V3["Virtual Thread N...\nmillions possible"]
V1 & V2 & V3 -.->|multiplexed onto| C["Small pool of\ncarrier threads"]
end
Pinning gotcha — synchronized blocks pin a virtual thread to its carrier during blocking calls (fixed later in Java 24's JEP 491, but relevant here):
// ⚠️ Avoid: synchronized + blocking I/O pins the carrier thread, reducing scalability
synchronized (lock) {
String result = blockingHttpCall(); // pins carrier thread until this returns
}
// ✅ Prefer: ReentrantLock, which doesn't pin
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
String result = blockingHttpCall(); // carrier thread is freed normally
} finally {
lock.unlock();
}sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, double radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
record Point(int x, int y) {}
static double area(Shape shape) {
return switch (shape) {
case Circle(Point(var cx, var cy), var r) -> Math.PI * r * r;
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) ->
Math.abs((x2 - x1) * (y2 - y1));
};
// No default needed — Shape is sealed, compiler verifies exhaustiveness!
}sealed interface PaymentEvent permits Authorized, Captured, Refunded, Failed {}
record Authorized(String txId, double amount) implements PaymentEvent {}
record Captured(String txId) implements PaymentEvent {}
record Refunded(String txId, double amount) implements PaymentEvent {}
record Failed(String txId, String reason) implements PaymentEvent {}
static String handle(PaymentEvent event) {
return switch (event) {
case Authorized(var id, var amt) when amt > 10_000 -> "Large auth: " + id + " needs review";
case Authorized(var id, var amt) -> "Authorized " + id + " for $" + amt;
case Captured(var id) -> "Captured: " + id;
case Refunded(var id, var amt) -> "Refunded $" + amt + " for " + id;
case Failed(var id, var reason) -> "Failed " + id + ": " + reason;
// Compiler ERROR if any sealed permit is missing — true exhaustiveness checking
};
}flowchart TD
A["sealed interface PaymentEvent"] --> B[Authorized]
A --> C[Captured]
A --> D[Refunded]
A --> E[Failed]
F["switch (event) { ... }"] -->|"Compiler verifies\nALL 4 cases handled"| G["✅ Compile-time\nexhaustiveness guarantee"]
What: A new collection interface hierarchy fixing a decades-old gap — no consistent way to get the "first"/"last" element or reversed view across List, Deque, LinkedHashSet, etc.
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a" — no more list.get(0)
list.getLast(); // "c" — no more list.get(list.size() - 1)
list.addFirst("z"); // [z, a, b, c]
list.addLast("d"); // [z, a, b, c, d]
List<String> reversed = list.reversed(); // [d, c, b, a, z] — reversed view
// Works uniformly across LinkedHashSet, LinkedHashMap too
LinkedHashSet<String> set = new LinkedHashSet<>(List.of("x", "y", "z"));
set.getFirst(); // "x"
set.reversed(); // view: [z, y, x]
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.sequencedKeySet().getFirst(); // "a"classDiagram
class SequencedCollection {
<<interface>>
+getFirst()
+getLast()
+addFirst(e)
+addLast(e)
+reversed()
}
class List
class Deque
class LinkedHashSet
SequencedCollection <|-- List
SequencedCollection <|-- Deque
SequencedCollection <|-- LinkedHashSet
String name = "Neelu";
int age = 34;
// Before: String.format or concatenation
String s1 = String.format("Hello %s, you are %d years old", name, age);
String s2 = "Hello " + name + ", you are " + age + " years old";
// String Templates (Preview) — embed expressions directly
String s3 = STR."Hello \{name}, you are \{age} years old";
// Works with any expression, not just variables
String s4 = STR."Total: \{price * quantity}";
// Note: withdrawn after Java 21 preview and redesigned —
// verify current syntax against the JDK version you're targeting
⚠️ Note: String Templates were later withdrawn from the JDK roadmap for redesign after the Java 21/22 previews — check current status before relying on this in new code.
See the Java 19 and Java 20 guides for the concepts — both continue as previews in 21, standardizing in later releases.
What: ZGC gains generational collection (young/old generations), dramatically improving throughput for typical allocation patterns (most objects die young) while keeping ZGC's signature sub-millisecond pause times.
java -XX:+UseZGC -XX:+ZGenerational -Xmx8g -jar app.jar
# (Generational mode became the default in Java 23 — see that guide)✅ Adopt Java 21 as your production LTS baseline today — it's the version with the longest runway and richest feature set
✅ Turn on spring.threads.virtual.enabled: true for I/O-heavy Spring Boot services and load test — often a big throughput win with zero code changes
✅ Model business events/results as sealed hierarchies of records, then handle them with exhaustive switch pattern matching — eliminates a whole class of "forgot to handle a case" bugs
✅ Replace list.get(0) / list.get(list.size()-1) with getFirst()/getLast() for readability
❌ Don't use synchronized blocks around blocking I/O on virtual threads — use ReentrantLock to avoid carrier-thread pinning
❌ Don't treat virtual threads as a magic performance fix for CPU-bound work — they only help I/O-bound concurrency
| Change | Action |
|---|---|
| Virtual threads available | Opt-in per executor/framework config — not automatic |
| Record patterns / switch pattern matching standard | Refactor instanceof chains and old-style switches where it improves clarity |
| Sequenced Collections | New default methods — no breaking changes, purely additive |
| String Templates preview → later withdrawn | Don't build production code on preview String Templates from 21 |
SecurityManager deprecated further |
Continue migration off it (fully removed in 24) |
flowchart LR
A[Java 17 App] --> B{I/O-bound service\nwith thread-pool bottlenecks?}
B -->|Yes| C[Enable virtual threads\nload test throughput]
B -->|No, CPU-bound| D[Virtual threads won't help—skip]
C --> E[Adopt sealed+record+switch\nfor domain modeling]
D --> E
E --> F[Deploy on Java 21 LTS]