Java 21 Features - Complete Guide
Comprehensive guide to Java 21 LTS features including Virtual Threads, Pattern Matching for Switch, Record Patterns, Sequenced Collections, and more with diagrams, data flows, and real-world examples.
Java 21 Features - Complete Guide
Java 21, released in September 2023, is the latest Long-Term Support (LTS) release featuring revolutionary improvements, especially Virtual Threads (Project Loom) that transform concurrent programming in Java.
Why Java 21 Matters
- Virtual Threads: Millions of lightweight threads
- Pattern Matching: Enhanced switch with patterns
- Sequenced Collections: Predictable ordering APIs
- Record Patterns: Destructuring for records
- Performance: Major runtime improvements
- LTS Support: Production-ready until 2031
Virtual Threads
Virtual threads are lightweight threads managed by the JVM, enabling massive scalability for concurrent applications.
Traditional vs Virtual Threads
Platform Threads:
┌─────────────────────┐
│ OS Thread (~1MB) │
│ ┌─────────────────┐ │
│ │ Java Thread │ │
│ └─────────────────┘ │
└─────────────────────┘
Limit: ~5,000 threads
Virtual Threads:
┌─────────────────────┐
│ Platform Thread │
│ ┌──┐┌──┐┌──┐┌──┐ │
│ │V1││V2││V3││V4│...│
│ └──┘└──┘└──┘└──┘ │
└─────────────────────┘
Scale: Millions of threads
Creating Virtual Threads
import java.util.concurrent.*;
public class VirtualThreadExamples {
public static void main(String[] args) throws InterruptedException {
// 1. Simple virtual thread
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread!");
});
vThread.join();
// 2. Named virtual thread
Thread named = Thread.ofVirtual()
.name("worker-thread")
.start(() -> System.out.println("Named thread"));
named.join();
// 3. ExecutorService with virtual threads
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Task in virtual thread");
return "Result";
});
}
}
}
Performance Comparison
import java.time.*;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class VirtualThreadPerformance {
private static void simulateIO() {
try {
Thread.sleep(Duration.ofMillis(100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void platformThreads(int tasks) {
Instant start = Instant.now();
try (ExecutorService executor = Executors.newFixedThreadPool(200)) {
IntStream.range(0, tasks)
.forEach(i -> executor.submit(() -> simulateIO()));
}
System.out.println("Platform: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
public static void virtualThreads(int tasks) {
Instant start = Instant.now();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, tasks)
.forEach(i -> executor.submit(() -> simulateIO()));
}
System.out.println("Virtual: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
public static void main(String[] args) {
int tasks = 10000;
virtualThreads(tasks); // ~100-200ms
platformThreads(tasks); // ~5000ms+
}
}
Real-World Use Cases
import java.util.*;
import java.util.concurrent.*;
public class VirtualThreadUseCases {
// Web server handling requests
public static void handleWebRequests(List<String> requests) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
requests.forEach(req ->
executor.submit(() -> processRequest(req))
);
}
}
// Parallel API calls
public static List<String> fetchAPIs(List<String> urls) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
return urls.stream()
.map(url -> executor.submit(() -> callAPI(url)))
.map(future -> {
try { return future.get(); }
catch (Exception e) { return "Error"; }
})
.toList();
}
}
// Database queries
public static List<String> parallelQueries(List<String> queries) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
return queries.stream()
.map(q -> executor.submit(() -> executeQuery(q)))
.map(f -> {
try { return f.get(); }
catch (Exception e) { return "Failed"; }
})
.toList();
}
}
private static void processRequest(String req) {
System.out.println("Processing: " + req);
}
private static String callAPI(String url) {
try { Thread.sleep(100); }
catch (InterruptedException e) {}
return "Response from " + url;
}
private static String executeQuery(String query) {
try { Thread.sleep(50); }
catch (InterruptedException e) {}
return "Result: " + query;
}
}
Pattern Matching for Switch
Enhanced switch with pattern matching and guards.
Pattern Matching Examples
public class PatternMatchingSwitch {
// Basic patterns
public static String formatValue(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
case Double d -> "Double: " + d;
case null -> "null";
default -> "Unknown: " + obj.getClass();
};
}
// With guards
public static String classify(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "Positive: " + i;
case Integer i when i < 0 -> "Negative: " + i;
case Integer i -> "Zero";
case String s when s.length() > 5 -> "Long string";
case String s -> "Short string";
default -> "Other";
};
}
// Sealed classes
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
public static double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
}
}
Record Patterns
Destructure records in pattern matching.
Record Pattern Examples
public record Point(int x, int y) {}
public record Rectangle(Point topLeft, Point bottomRight) {}
public class RecordPatterns {
// Basic destructuring
public static void printPoint(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println("Point: (" + x + ", " + y + ")");
}
}
// In switch
public static String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == 0 -> "Y-axis";
case Point(int x, int y) when y == 0 -> "X-axis";
case Point(int x, int y) -> "Point (" + x + "," + y + ")";
default -> "Not a point";
};
}
// Nested patterns
public static String describeRect(Object obj) {
return switch (obj) {
case Rectangle(Point(int x1, int y1), Point(int x2, int y2)) ->
"Rectangle from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
default -> "Not a rectangle";
};
}
public static void main(String[] args) {
printPoint(new Point(3, 4));
System.out.println(describe(new Point(0, 0)));
System.out.println(describeRect(
new Rectangle(new Point(0, 0), new Point(10, 10))
));
}
}
Sequenced Collections
Uniform API for collections with defined order.
Sequenced Collection API
SequencedCollection<E>
├─ addFirst(E)
├─ addLast(E)
├─ getFirst()
├─ getLast()
├─ removeFirst()
├─ removeLast()
└─ reversed()
Examples
import java.util.*;
public class SequencedCollections {
public static void main(String[] args) {
// List operations
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
list.addFirst("Z"); // [Z, A, B, C]
list.addLast("D"); // [Z, A, B, C, D]
System.out.println("First: " + list.getFirst()); // Z
System.out.println("Last: " + list.getLast()); // D
list.removeFirst(); // [A, B, C, D]
list.removeLast(); // [A, B, C]
List<String> reversed = list.reversed();
System.out.println("Reversed: " + reversed); // [C, B, A]
// Set operations
SequencedSet<Integer> set = new LinkedHashSet<>(List.of(1, 2, 3));
set.addFirst(0);
set.addLast(4);
System.out.println("Set: " + set); // [0, 1, 2, 3, 4]
// Map operations
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.putFirst("zero", 0);
map.putLast("three", 3);
System.out.println("First: " + map.firstEntry()); // zero=0
System.out.println("Last: " + map.lastEntry()); // three=3
}
}
LRU Cache Example
import java.util.*;
public class LRUCache<K, V> {
private final int capacity;
private final SequencedMap<K, V> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public V get(K key) {
V value = cache.remove(key);
if (value != null) {
cache.putLast(key, value); // Move to end
}
return value;
}
public void put(K key, V value) {
cache.remove(key);
cache.putLast(key, value);
if (cache.size() > capacity) {
cache.pollFirstEntry(); // Remove oldest
}
}
public static void main(String[] args) {
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("1", "One");
cache.put("2", "Two");
cache.put("3", "Three");
System.out.println(cache.cache); // {1=One, 2=Two, 3=Three}
cache.get("1"); // Access 1
System.out.println(cache.cache); // {2=Two, 3=Three, 1=One}
cache.put("4", "Four"); // Evicts 2
System.out.println(cache.cache); // {3=Three, 1=One, 4=Four}
}
}
Real-World Examples
High-Throughput Web Server
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class VirtualThreadServer {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(8080);
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println("Server started on port 8080");
while (true) {
Socket client = server.accept();
executor.submit(() -> handleRequest(client));
}
}
}
private static void handleRequest(Socket socket) {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String request = in.readLine();
System.out.println("Request: " + request);
// Simulate processing
Thread.sleep(100);
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/plain");
out.println();
out.println("Hello from Virtual Thread!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Microservice Orchestration
import java.util.*;
import java.util.concurrent.*;
public record UserData(String id, String name) {}
public record OrderData(String id, double total) {}
public record PaymentData(String id, boolean verified) {}
public record AggregatedData(
UserData user,
List<OrderData> orders,
PaymentData payment
) {}
public class MicroserviceOrchestrator {
public static AggregatedData fetchUserProfile(String userId) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<UserData> userFuture =
executor.submit(() -> fetchUser(userId));
Future<List<OrderData>> ordersFuture =
executor.submit(() -> fetchOrders(userId));
Future<PaymentData> paymentFuture =
executor.submit(() -> fetchPayment(userId));
try {
return new AggregatedData(
userFuture.get(),
ordersFuture.get(),
paymentFuture.get()
);
} catch (Exception e) {
throw new RuntimeException("Failed to fetch profile", e);
}
}
}
private static UserData fetchUser(String id) {
sleep(100);
return new UserData(id, "Venu");
}
private static List<OrderData> fetchOrders(String id) {
sleep(150);
return List.of(new OrderData("O1", 100.0));
}
private static PaymentData fetchPayment(String id) {
sleep(120);
return new PaymentData("P1", true);
}
private static void sleep(int ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
AggregatedData data = fetchUserProfile("user123");
long end = System.currentTimeMillis();
System.out.println("User: " + data.user().name());
System.out.println("Orders: " + data.orders().size());
System.out.println("Time: " + (end - start) + "ms");
// ~150ms (parallel) vs ~370ms (sequential)
}
}
Best Practices
Virtual Threads
// ✅ Good: I/O-bound tasks
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> fetchFromDatabase());
executor.submit(() -> callAPI());
}
// ❌ Avoid: CPU-intensive tasks
// Use platform threads or ForkJoinPool
// ✅ Good: Don't pool virtual threads
Thread.startVirtualThread(() -> task());
// ❌ Avoid: Pooling defeats the purpose
ExecutorService pool = Executors.newFixedThreadPool(100,
Thread.ofVirtual().factory());
Pattern Matching
// ✅ Good: Exhaustive with sealed classes
public double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
// No default needed - compiler ensures coverage
};
}
// ✅ Good: Use guards for conditions
public String classify(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "Positive";
case Integer i when i < 0 -> "Negative";
case Integer i -> "Zero";
default -> "Not an integer";
};
}
Migration from Java 17
// 1. Replace thread pools with virtual threads
// Before
ExecutorService executor = Executors.newFixedThreadPool(100);
// After
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 2. Use enhanced pattern matching
// Before
if (obj instanceof String) {
String s = (String) obj;
if (s.length() > 5) {
process(s);
}
}
// After
if (obj instanceof String s && s.length() > 5) {
process(s);
}
// 3. Use record patterns
// Before
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
}
// After
if (obj instanceof Point(int x, int y)) {
// Use x and y directly
}
// 4. Use sequenced collections
// Before
list.add(0, "first");
String first = list.get(0);
// After
list.addFirst("first");
String first = list.getFirst();
Summary
Java 21 LTS brings revolutionary features:
✅ Virtual Threads - Millions of lightweight threads ✅ Pattern Matching - Enhanced switch expressions ✅ Record Patterns - Destructuring for records ✅ Sequenced Collections - Predictable ordering ✅ Performance - Significant improvements ✅ LTS Support - Production-ready until 2031
Key Benefits
- Scalability: Handle millions of concurrent tasks
- Simplicity: Write concurrent code like sequential
- Safety: Better type checking with patterns
- Productivity: Less boilerplate, cleaner code
- Performance: Improved runtime efficiency
When to Use
- Virtual Threads: I/O-bound applications, web servers, microservices
- Pattern Matching: Type-safe data processing, domain modeling
- Record Patterns: Data extraction, API responses
- Sequenced Collections: Ordered data structures, caches
Remember: Java 21 is the recommended LTS version for new projects. Virtual threads alone make it a game-changer for concurrent applications! 🚀