Full Stack • Java • System Design • Cloud • AI Engineering

Java2026-06-07

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! 🚀