Records and sealed classes are syntactic sugar that make certain common Java patterns more concise and robust, while virtual threads are a fundamental shift in how concurrency is managed.

Let’s see records and sealed classes in action. Imagine you have a Point class that just holds x and y coordinates.

// Old way
class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" +
               "x=" + x +
               ", y=" + y +
               '}';
    }
}

With Java 21 records, this entire boilerplate is gone:

// Java 21 Record
record Point(int x, int y) {}

This record Point(int x, int y) {} automatically gives you a constructor, getX(), getY(), equals(), hashCode(), and toString(). It’s immutable by design.

Sealed classes help you restrict which other classes can extend or implement them. Consider a Shape hierarchy.

// Java 21 Sealed Class
sealed class Shape permits Circle, Square, Rectangle {
    abstract double area();
}

final class Circle extends Shape {
    double radius;

    Circle(double radius) { this.radius = radius; }

    @Override
    double area() { return Math.PI * radius * radius; }
}

final class Square extends Shape {
    double side;

    Square(double side) { this.side = side; }

    @Override
    double area() { return side * side; }
}

// Note: Rectangle is permitted but not final, so it can be extended further.
class Rectangle extends Shape {
    double width;
    double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double area() { return width * height; }
}

The permits clause explicitly lists all direct subclasses. Any attempt to extend Shape without being listed in permits will fail at compile time. This makes your type hierarchies more explicit and safer.

Now, virtual threads. Before Java 21, if you had 1000 concurrent operations, you’d likely use a fixed-size thread pool (e.g., Executors.newFixedThreadPool(200)). Each task would be assigned a platform thread. If one task blocked (e.g., waiting for I/O), its platform thread would be blocked, unable to do other work. This often led to needing many threads, which consume significant memory and context-switching overhead.

Virtual threads, introduced as a preview feature in earlier versions and finalized in Java 21, change this. They are lightweight threads managed by the JVM, not directly by the operating system. You can create millions of them.

Here’s how you’d use them for a simple web request simulation:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.StructuredTaskScope;
import java.util.stream.IntStream;

public class VirtualThreadDemo {

    public static void main(String[] args) throws Exception {
        // Using StructuredTaskScope for managing virtual threads
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            // Submit tasks to run concurrently using virtual threads
            var futures = IntStream.range(0, 1000).mapToObj(i -> scope.fork(() -> {
                String url = "https://httpbin.org/delay/" + (i % 5); // Simulate varying delays
                System.out.println("Fetching: " + url);
                HttpClient client = HttpClient.newBuilder()
                        .connectTimeout(Duration.ofSeconds(10))
                        .build();
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(url))
                        .timeout(Duration.ofSeconds(15))
                        .build();
                try {
                    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                    // System.out.println("Response from " + url + ": " + response.statusCode());
                    return url + " status: " + response.statusCode();
                } catch (Exception e) {
                    System.err.println("Error fetching " + url + ": " + e.getMessage());
                    throw e; // Rethrow to signal failure in the scope
                }
            })).toList();

            scope.join(); // Wait for all tasks to complete or one to fail
            scope.throwIfFailed(); // Throw an exception if any task failed

            System.out.println("All tasks completed successfully.");
            // You can process results from futures if needed
            // for (Future<String> future : futures) {
            //     System.out.println(future.resultNow());
            // }
        }
    }
}

In this example, scope.fork(() -> { ... }) automatically runs the lambda in a virtual thread. When client.send(request, ...) is called, if it needs to perform blocking I/O, the virtual thread unmounts from its underlying OS thread. The OS thread is then free to run another virtual thread. When the I/O operation completes, the virtual thread remounts onto an available OS thread to continue execution. This allows a small number of OS threads to handle a massive number of concurrent, potentially blocking, operations efficiently.

StructuredTaskScope is a powerful API introduced alongside virtual threads for managing groups of concurrent tasks. ShutdownOnFailure means if any task in the scope fails, all other running tasks are cancelled, and the scope will throw an exception summarizing the failures.

The most surprising thing about virtual threads is how they decouple the concurrency you write from the underlying OS threads. You can write imperative, blocking code that looks like traditional single-threaded execution, but under the hood, the JVM is efficiently multiplexing these "virtual" threads onto a pool of OS threads. This means you can often achieve massive scalability without rewriting your code to be fully asynchronous and non-blocking, which is notoriously complex.

The biggest lever you control with records is immutability and data integrity. For sealed classes, it’s about creating well-defined, exhaustive type hierarchies. For virtual threads, it’s about dramatically simplifying concurrent programming while improving resource utilization.

The next logical step after mastering these features is exploring the full potential of StructuredTaskScope and its various shutdown policies.

Want structured learning?

Take the full Java course →