Java 21 Features

Java 21 is a Long-Term Support (LTS) release and one of the most impactful Java versions in years. It brings major improvements to concurrency, performance, and developer productivity, especially through Virtual Threads and Structured Concurrency.


1) Virtual Threads (JEP 444) — Lightweight Concurrency (Production Ready)

What it solves

Traditional Java threads are heavy. When you build high-concurrency servers (APIs, microservices), using one platform thread per request does not scale well. Virtual threads are managed by the JVM and are much cheaper, enabling massive concurrency with simple “blocking style” code.

When to use

  • Web servers (Tomcat/Jetty/Undertow)
  • Massive IO workloads (HTTP calls, DB calls, file/network operations)
  • Systems currently using reactive frameworks only to avoid thread blocking

Example: Creating virtual threads

public class VirtualThreadsDemo {
    public static void main(String[] args) throws Exception {
        try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 5; i++) {
                int taskId = i;
                executor.submit(() -> {
                    Thread.sleep(500); // blocking is okay with virtual threads
                    System.out.println("Task " + taskId + " on " + Thread.currentThread());
                    return taskId;
                });
            }
        }
    }
}

Tip

Virtual threads shine when your code blocks on IO. They don’t help much for CPU-heavy tasks.


2) Structured Concurrency (JEP 453) — Manage Concurrent Tasks as One Unit (Preview)

What it solves

When you spawn multiple tasks (fetch user + orders + profile), you want:

  • A single scope/lifetime
  • Proper cancellation on failure
  • Clean error handling

Structured concurrency makes concurrent logic readable and safe.

Example: Run tasks together and fail fast

import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;

public class StructuredConcurrencyDemo {

    static String fetchUser() throws InterruptedException {
        Thread.sleep(300);
        return "Susil";
    }

    static String fetchOrders() throws InterruptedException {
        Thread.sleep(500);
        return "Orders: 12";
    }

    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

            var userTask = scope.fork(StructuredConcurrencyDemo::fetchUser);
            var ordersTask = scope.fork(StructuredConcurrencyDemo::fetchOrders);

            scope.join();               // wait for both
            scope.throwIfFailed();      // propagate exceptions

            System.out.println("User: " + userTask.get());
            System.out.println("Orders: " + ordersTask.get());
        }
    }
}

Run note (Preview feature)

Preview features require flags:

  • --enable-preview --release 21

3) Scoped Values (JEP 446) — Safer Alternative to ThreadLocal (Preview)

What it solves

ThreadLocal becomes tricky with async code, thread pools, and modern concurrency. Scoped Values provide immutable, safe context propagation—especially useful with virtual threads.

Example: Request context without ThreadLocal

import jdk.incubator.concurrent.ScopedValue;

public class ScopedValuesDemo {

    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    static void handleRequest() {
        System.out.println("RequestId = " + REQUEST_ID.get());
    }

    public static void main(String[] args) {
        ScopedValue.where(REQUEST_ID, "REQ-2026-0001").run(ScopedValuesDemo::handleRequest);
    }
}

Note: APIs may differ slightly depending on finalization. Scoped Values in Java 21 are Preview.


4) Pattern Matching for switch (JEP 441) — More Expressive switch (Final)

What it solves

You can write cleaner conditional logic on types and values without verbose if/else chains.

Example: Type patterns + guards

public class SwitchPatternDemo {
    static String format(Object obj) {
        return switch (obj) {
            case null -> "null value";
            case Integer i -> "int: " + (i * 2);
            case String s && s.isBlank() -> "blank string";
            case String s -> "text: " + s.toUpperCase();
            default -> "other: " + obj;
        };
    }

    public static void main(String[] args) {
        System.out.println(format(10));
        System.out.println(format("  "));
        System.out.println(format("java"));
    }
}

5) Record Patterns (JEP 440) — Destructure Records (Final)

What it solves

Records are great, but unpacking nested records was still verbose. Record patterns allow destructuring inside instanceof and switch.

Example: Destructure record in one line

record Point(int x, int y) {}
record Line(Point start, Point end) {}

public class RecordPatternsDemo {
    static int lengthX(Line line) {
        if (line instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
            return Math.abs(x2 - x1);
        }
        return 0;
    }

    public static void main(String[] args) {
        var l = new Line(new Point(2, 5), new Point(9, 1));
        System.out.println(lengthX(l));
    }
}

6) Sequenced Collections (JEP 431) — First/Last Operations (Final)

What it solves

Java collections finally standardize “ordered collections” operations like:

  • get first / last
  • add first / last
  • reversed view

Example: Deque-like behavior in List

import java.util.ArrayList;
import java.util.SequencedCollection;

public class SequencedCollectionsDemo {
    public static void main(String[] args) {
        SequencedCollection<String> names = new ArrayList<>();
        names.add("A");
        names.add("B");
        names.add("C");

        System.out.println(names.getFirst()); // A
        System.out.println(names.getLast());  // C
        System.out.println(names.reversed()); // [C, B, A]
    }
}

7) String Templates (JEP 430) — Easier Interpolation (Preview, May Change)

What it solves

Safer and cleaner string formatting than manual concatenation or String.format.

Example (Preview)

// Preview feature - syntax may change in later versions
String name = "Susil";
int ver = 21;

// Example style:
String msg = STR."Hello \{name}, welcome to Java \{ver}!";
System.out.println(msg);

Since this is Preview, consider mentioning in your blog that the feature may evolve.


8) Unnamed Patterns and Variables (JEP 443) — Ignore What You Don’t Need (Preview)

What it solves

Sometimes you don’t care about a variable. Java 21 introduces _ to indicate “unused”.

Example

record User(String name, int age) {}

public class UnnamedVarDemo {
    static void printName(User user) {
        if (user instanceof User(String name, int _)) { // ignore age
            System.out.println(name);
        }
    }
}

9) Unnamed Classes and Instance Main Methods (JEP 445) — Faster “Hello World” (Preview)

What it solves

For quick demos, scripts, or teaching beginners, you can write small programs with less boilerplate.

Example (Preview)

void main() {
    System.out.println("Java 21 is awesome!");
}

Java 21 Upgrade Notes (Practical Tips)

Recommended JDK

  • Use a trusted distribution: Temurin / Oracle JDK / Microsoft Build of OpenJDK

Preview features

To compile/run preview features:

  • Compile: javac --enable-preview --release 21 MyFile.java
  • Run: java --enable-preview MyFile

Spring Boot compatibility

If you’re on Spring Boot 3.x, Java 21 is commonly supported in newer minor releases—upgrade Spring Boot + plugins accordingly.


Java 21 Feature Summary (Quick Table)

  • Virtual Threads — Final ✅
  • Pattern Matching for switch — Final ✅
  • Record Patterns — Final ✅
  • Sequenced Collections — Final ✅
  • Structured Concurrency — Preview 🧪
  • Scoped Values — Preview 🧪
  • String Templates — Preview 🧪
  • Unnamed patterns/variables — Preview 🧪
  • Unnamed classes & instance main — Preview 🧪

Conclusion

Java 21 is a must-consider LTS upgrade because it modernizes concurrency with virtual threads and improves readability with pattern matching and record patterns. If you build backend services, Java 21 can deliver significant scalability improvements with minimal code changes.


FAQ

Is Java 21 stable for production?
Yes—Java 21 is an LTS release. Final features are production-ready. Preview features are optional and may change.

Do virtual threads replace reactive programming?
Not always, but for many IO-heavy services, virtual threads allow simple blocking code with high scalability.