Java 21 (September 2023)¶
Release Information
Release Date: September 19, 2023 End of Support: September 2031 Theme: Concurrency Revolution
Overview¶
Java 21 is a landmark release that fundamentally changes how we write concurrent Java code. Virtual Threads enable handling millions of concurrent tasks with familiar synchronous code, while pattern matching reaches its full potential.
Virtual Threads¶
Interview Favorite
This is THE most important feature in Java 21. Understand the difference from platform threads and when to use them.
The Problem¶
Platform threads are expensive (~1MB memory each), limiting applications to thousands of concurrent connections.
Before/After¶
// Platform threads - expensive, limited
ExecutorService executor = Executors.newFixedThreadPool(200); // (1)
for (int i = 0; i < 10_000; i++) {
final int taskId = i;
executor.submit(() -> {
// Each task holds an expensive OS thread while waiting
Thread.sleep(Duration.ofSeconds(1)); // (2)
callExternalApi();
return processResult();
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
- Limited to ~200 due to memory (~200MB for threads alone)
- Thread blocked but still consuming resources
// Virtual threads - lightweight, millions possible
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) { // (1)
final int taskId = i;
executor.submit(() -> {
// Virtual thread unmounts during blocking
Thread.sleep(Duration.ofSeconds(1)); // (2)
callExternalApi();
return processResult();
});
}
} // (3)
- One million tasks - no problem!
- Thread unmounts from carrier, nearly zero cost
- Auto-closes and waits for completion
How Virtual Threads Work¶
┌─────────────────────────────────────────────────────────────┐
│ PLATFORM THREADS │
│ (Few - matches CPU cores, e.g., 8) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ PT1 │ │ PT2 │ │ PT3 │ │ PT4 │ │ PT5 │ │ PT6 │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │ │ │
├─────┼────────┼────────┼────────┼────────┼────────┼──────────┤
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ VT1 │ │ VT4 │ │ VT7 │ │VT10 │ │VT13 │ │VT16 │ │
│ │ VT2 │ │ VT5 │ │ VT8 │ │VT11 │ │VT14 │ │VT17 │ │
│ │ VT3 │ │ VT6 │ │ VT9 │ │VT12 │ │VT15 │ │VT18 │ │
│ │ ... │ │ ... │ │ ... │ │ ... │ │ ... │ │ ... │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ VIRTUAL THREADS (Millions) │
└─────────────────────────────────────────────────────────────┘
When VT1 blocks (I/O, sleep), it unmounts from PT1.
Another virtual thread (e.g., VT2) mounts on PT1.
When VT1's I/O completes, it remounts on any available PT.
Creating Virtual Threads¶
// Option 1: ExecutorService (recommended for most cases)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest(request));
}
// Option 2: Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> doWork());
// Option 3: Thread.startVirtualThread()
Thread.startVirtualThread(() -> doWork());
// Check if current thread is virtual
if (Thread.currentThread().isVirtual()) {
System.out.println("Running on virtual thread");
}
Comparison Table¶
| Aspect | Platform Threads | Virtual Threads |
|---|---|---|
| Memory | ~1MB per thread | ~1KB per thread |
| Creation time | Milliseconds | Microseconds |
| Max concurrent | ~10,000 | Millions |
| Best for | CPU-bound work | I/O-bound work |
| Blocking cost | High (wastes resources) | Near zero |
| Pooling | Required | Not recommended |
Key Points¶
Benefits
- Write synchronous code, get async scalability
- Existing
ThreadAPI works unchanged - Debugging and profiling tools work normally
- Stack traces are complete and readable
Caveats
- Don't pool virtual threads - create new ones per task
- Avoid
synchronized- useReentrantLockinstead (synchronized pins to carrier thread) - CPU-bound tasks don't benefit - use platform threads
- ThreadLocal can leak memory - use Scoped Values instead
Pattern Matching for switch (Final)¶
Interview Favorite
Know null handling, guarded patterns with when, and exhaustiveness checking.
The Problem¶
Complex type checking required chains of if-else instanceof.
Before/After¶
String describe(Object obj) {
if (obj == null) {
return "null";
} else if (obj instanceof Integer i) {
return "Integer: " + i;
} else if (obj instanceof Long l) {
return "Long: " + l;
} else if (obj instanceof String s) {
if (s.isEmpty()) {
return "Empty string";
} else {
return "String: " + s;
}
} else if (obj instanceof List<?> list) {
if (list.isEmpty()) {
return "Empty list";
} else {
return "List with " + list.size() + " elements";
}
} else {
return "Unknown: " + obj.getClass().getName();
}
}
String describe(Object obj) {
return switch (obj) {
case null -> "null"; // (1)
case Integer i -> "Integer: " + i;
case Long l -> "Long: " + l;
case String s when s.isEmpty() -> "Empty string"; // (2)
case String s -> "String: " + s;
case List<?> list when list.isEmpty() -> "Empty list";
case List<?> list -> "List with " + list.size() + " elements";
default -> "Unknown: " + obj.getClass().getName();
};
}
nullcan be a case label- Guarded patterns with
whenclause
With Sealed Classes (Exhaustive)¶
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Compiler verifies all cases covered - no default needed!
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
};
}
Pattern Dominance¶
// Order matters - more specific patterns first
switch (obj) {
case String s when s.isEmpty() -> "empty"; // More specific
case String s -> "non-empty: " + s; // Less specific
default -> "other";
}
// This would be a compile error:
// case String s -> ... // Dominates next case
// case String s when ... -> ... // Never reached!
Record Patterns¶
The Problem¶
Extracting data from nested records required multiple steps.
Before/After¶
record Point(int x, int y) {}
record Line(Point start, Point end) {}
void processLine(Object obj) {
if (obj instanceof Line line) {
Point start = line.start();
Point end = line.end();
int x1 = start.x();
int y1 = start.y();
int x2 = end.x();
int y2 = end.y();
double length = Math.sqrt(
Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)
);
System.out.println("Length: " + length);
}
}
record Point(int x, int y) {}
record Line(Point start, Point end) {}
void processLine(Object obj) {
// Nested deconstruction in one pattern!
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.println("Length: " + length);
}
}
In Switch¶
sealed interface Expr permits Const, Add, Mul {}
record Const(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
int eval(Expr expr) {
return switch (expr) {
case Const(int v) -> v;
case Add(var l, var r) -> eval(l) + eval(r); // var allowed
case Mul(var l, var r) -> eval(l) * eval(r);
};
}
// Even more complex patterns
String describe(Object obj) {
return switch (obj) {
case Line(Point(int x, int y), Point(0, 0)) ->
"Line from (" + x + "," + y + ") to origin";
case Line(Point(0, 0), Point(int x, int y)) ->
"Line from origin to (" + x + "," + y + ")";
case Line l ->
"Generic line: " + l;
default -> "Not a line";
};
}
Sequenced Collections¶
The Problem¶
No uniform way to access first/last elements across collection types.
Before/After¶
// Different APIs for different collections
List<String> list = new ArrayList<>();
String first = list.get(0);
String last = list.get(list.size() - 1);
Deque<String> deque = new ArrayDeque<>();
String first2 = deque.getFirst();
String last2 = deque.getLast();
SortedSet<String> sortedSet = new TreeSet<>();
String first3 = sortedSet.first();
String last3 = sortedSet.last();
LinkedHashSet<String> linkedSet = new LinkedHashSet<>();
// No easy way to get first/last!
String first4 = linkedSet.iterator().next();
// last? Need to iterate entire set...
// Uniform interface for all ordered collections
SequencedCollection<String> coll = // List, Deque, LinkedHashSet, etc.
String first = coll.getFirst();
String last = coll.getLast();
coll.addFirst("new first");
coll.addLast("new last");
String removedFirst = coll.removeFirst();
String removedLast = coll.removeLast();
// Reversed view - not a copy!
SequencedCollection<String> reversed = coll.reversed();
for (String s : reversed) {
// Iterates in reverse order
}
New Interface Hierarchy¶
Collection
│
SequencedCollection ←── NEW
│ │
List Deque
│
SequencedSet ←── NEW
│
SortedSet
Map
│
SequencedMap ←── NEW
│
SortedMap
SequencedMap¶
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
Map.Entry<String, Integer> first = map.firstEntry(); // a=1
Map.Entry<String, Integer> last = map.lastEntry(); // c=3
map.putFirst("z", 0); // Now first
map.putLast("d", 4); // Now last
Map.Entry<String, Integer> polled = map.pollFirstEntry();
SequencedMap<String, Integer> reversed = map.reversed();
String Templates (Preview)¶
Preview Feature
Available with --enable-preview flag. May change in future releases.
The Problem¶
String concatenation is error-prone, especially for SQL/HTML injection.
Example¶
// With preview enabled
String name = "Alice";
int age = 30;
// Basic template
String greeting = STR."Hello, \{name}! You are \{age} years old.";
// Expressions in templates
String info = STR."Next year you'll be \{age + 1}.";
// Multi-line
String json = STR."""
{
"name": "\{name}",
"age": \{age}
}
""";
// Custom template processor for safe SQL (hypothetical)
// PreparedStatement stmt = SQL."SELECT * FROM users WHERE name = \{name}";
Quick Reference¶
// Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest());
}
Thread.startVirtualThread(() -> doWork());
// Pattern Matching for switch
String result = switch (obj) {
case null -> "null";
case String s when s.isEmpty() -> "empty";
case String s -> "string: " + s;
case Integer i -> "int: " + i;
default -> "other";
};
// Record Patterns
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
// Sequenced Collections
SequencedCollection<String> coll = list;
coll.getFirst();
coll.getLast();
coll.reversed();