Your knowledge hub on nearshore software development | Pwrteams

Modern Java unleashed: virtual threads revolution & other game-changing features in JDK 21

Written by Admin | May 23, 2025

Today, we celebrate the 30th anniversary of one of the most enduring and influential programming languages – Java. Since its debut in 1995, Java has shaped the software development landscape, powering everything from web and mobile apps to enterprise systems and big data solutions. Its versatility, reliability and platform independence have made it a cornerstone of modern development.

To celebrate the occasion, we invited Ivan Mihov, Senior Java Engineer at News UK Tech team. With extensive experience in consulting and designing scalable systems, Ivan shares his insights into Java’s evolution and some of the key features in JDK 21.

From virtual threads to cleaner code constructs

Java 21 is more than a version bump - it's a leap toward modern concurrency, better collections, cleaner conditional and data access logic. While past releases added convenience, Java 21 introduces tools that can reshape how we write scalable, expressive and maintainable Java.

Virtual threads

What are Virtual threads?

Virtual threads, part of Project Loom, are a lightweight alternative to platform threads. Unlike traditional threads (1:1 with OS threads), virtual threads are managed entirely by the JVM, not the operating system.

Analogy: Traditional platform threads are like trucks that each carry packages to their destination and wait there until unloading is complete, even if that means waiting hours for someone to sign for it. During that waiting time, the truck is completely unavailable to deliver other packages.

Virtual threads are like a smart delivery system where trucks drop off packages and immediately become available for new deliveries, while a local service handles the waiting and paperwork.

The JVM schedules the virtual threads onto a small pool of carrier (real) threads, suspending them in memory when they block, rather than tying up an OS thread. When a virtual thread performs a blocking I/O operation, it can unmount from its carrier (platform thread). The carrier gets free, so the JVM can mount a different virtual thread on that carrier. That’s why the key efficiency comes from what happens when a virtual thread performs a blocking operation.

Also, this in-memory handling is what makes them so cheap and scalable. They allow us to write synchronous-style code that runs concurrently like asynchronous code.

Since virtual threads are mounted upon platform threads(carriers of the virtual threads), you may consider them an illusion that the JVM provides. They're essentially Java objects allocated on the heap that can be garbage collected when no longer needed.

In Summary, the key efficiency comes from what happens when a virtual thread performs a blocking operation. After running some code, the virtual thread can unmount from its carrier.

Opening a virtual thread is roughly 10x faster than a platform thread and uses around ~1–2 KB of memory compared to ~1MB for a platform thread. Platform threads require a large, contiguous allocation of memory (often around 1MB per thread) to hold their stack, while virtual threads store their stack frames in Java's garbage-collected heap rather than in these monolithic memory blocks and take around ~1KB of memory.

Example with Explicit Virtual threads

This creates a new thread for each task without the cost of creating an OS thread. You can run thousands, even millions of virtual threads concurrently.

 Spring Boot integration (zero code change)

Starting from Spring Boot 3.2, virtual threads can be enabled by configuration:

That’s it. Your controllers, services and database calls will now execute on virtual threads automatically:

Note: Even if userRepository runs in a lightweight virtual thread, because it uses JDBC, the carrier thread will still be blocked during the JDBC call. We will explore this limitation a bit later in the article.

Note: While the property enables virtual threads for web request handling, certain libraries or components might still need explicit configuration (as we will see in the next section for RestClient).

REST client example

While enabling virtual threads with spring.threads.virtual.enabled=true configures your Spring Boot application to handle incoming web requests using virtual threads, there's an important caveat: HTTP clients like RestClient or RestTemplate used within your application won't automatically use virtual threads.

For our simple test endpoint (/thread-info), this wasn't an issue because we weren't making any outgoing HTTP requests. But if you're building an API that calls other services, you'll need additional configuration:

Then you can use this RestClient in your controller:

Virtual threads vs CompletableFuture

Virtual threads offer a fundamentally different approach to concurrency compared to CompletableFuture. While CompletableFuture uses a callback-based programming style, virtual threads allow you to write synchronous-looking code that behaves asynchronously under the hood.

With CompletableFuture, you typically chain operations using methods like thenApply, thenCompose and thenAccept. Virtual threads, on the other hand, let you write simple imperative code that reads more naturally.

Spring data JPA in Virtual threads

Blocking operations like JDBC can run in virtual threads, but with significant limitations:

JDBC is blocking - there's no async or reactive JDBC API in the standard Java JDK (though there are third-party solutions like R2DBC). And with platform threads, this blocking is expensive.

Unfortunately, virtual threads don't fully solve this problem for JDBC. While virtual threads can handle thousands of concurrent operations, JDBC operations typically cause thread pinning. This is partly because many JDBC drivers use synchronised blocks internally, but also because native blocking calls themselves can pin the thread. As a result, the carrier thread remains occupied during database access, limiting scalability.

This means the carrier thread cannot be reused for other virtual threads during that time.

Other Virtual threads limitations

I/O vs. CPU-bound operations: Virtual threads provide significant benefits for I/O-bound applications but don't improve performance for CPU-intensive tasks. A benchmark comparing virtual threads to platform threads for CPU-intensive operations (calculating Fibonacci) showed little difference in throughput - Fast thread.

Thread pinning issues: Virtual threads can get "pinned" to carrier threads in certain scenarios, including when using synchronised blocks/methods or when executing blocking operations in native methods. Spring Framework uses synchronised blocks in various places, though they've been working to revise these to avoid pinning issues.

Framework limitations: Switching from normal threads to virtual threads can have unforeseen consequences for legacy applications, requiring thorough testing.

Sequenced collections 

The problem they solve

Prior to Java 21, Set and Map didn’t provide guaranteed first/last element access in a uniform way. SequencedCollection, SequencedSet and SequencedMap fix this by standardising ordered access.

Integration with Existing Classes: These new interfaces fit neatly into the existing collections hierarchy - List and Deque now extend SequencedCollection, LinkedHashSet implements SequencedSet and SortedSet has SequencedSet as its immediate superinterface. This means existing collection classes you already use gain these powerful methods automatically.

Example usage

This API brings clarity to cases where ordering matters but isn’t explicitly encoded in the type.

Common pitfalls

  • Using a regular HashSet assumes no order. Now you can enforce expectations with sequenced interfaces.
  • Some legacy code assumes ordering based on insertion – now it's formalised

Real use case: LRU cache

You now have predictable, spec-defined ordering for eviction strategies.

Audit trail buffer example

Sequenced collections are perfect for maintaining audit trails:

It improves both readability and intent when working with time-ordered operations.

Pattern matching for switch

Why it matters

Switches in Java used to be tied to primitives or enums. Now you can switch on object types, and the compiler does both the instanceof check and cast.

Example

Clean, concise, no explicit instanceof or casts.

Pitfalls

  • Must be exhaustive, or the compiler will force a default.
  • Doesn’t replace polymorphism when business logic varies per type.

Real use case: JSON dispatching

Improves readability in parsers and API gateways.

Domain rule evaluation

Pattern matching is excellent in DSLs or business rule engines:

The power of "when" guards: Pattern matching in switch becomes even more powerful with "when" clauses. These guards let you add arbitrary boolean conditions to pattern matches, combining type checking and value testing in a single step.

In this example, after matching Integer types and extracting the value into the "age" variable, the when clause filters matches based on their values, creating a clean alternative to nested if-else chains. This approach adds clarity to complex business rules and removes deeply nested conditional logic.

Record patterns

What they do

Record patterns let you deconstruct records inline, reducing boilerplate and making your code look declarative.

Seamless Backward Compatibility: Record patterns work seamlessly with all existing record classes - no modifications needed to your records. This means you can immediately start using pattern matching with records you've already defined in your codebase, unlocking a more declarative style without any migration effort.

Basic example

Compare that to using p.x() and p.y() after type checks and casting.

Match in switch

You can pair record patterns inside pattern matching for switch to write even more declarative code.

Real use case: event processing

This is perfect for modelling finite domain workflows like state machines or event buses.

Nested record unpacking

Record patterns allow matching deep structures in one step:

No need to extract and cast each component.

Summary table

Here's how these features transform Java development:

Feature Why It Matters Real-World Value
Virtual Threads Enables concurrency without complexity Run blocking code without killing scalability
Sequenced Collections Gives order to hashed data Clear APIs for LRU, FIFO queues, audit trails
Pattern Switch Simplifies logic based on type Faster, clearer decision trees in apps
Record Patterns Declarative data unpacking Easier DTO/event parsing, no boilerplate

 

Final words

Java 21 arms you with the tools to write modern, clean and scalable code without relying on external libraries or complex patterns. Virtual Threads make high-concurrency apps a reality for every Java team, even if developers don’t want to write async-style code, while pattern matching and record patterns strip away old verbosity. And sequenced collections? They finally bring order and an enhanced API to hashed data optimised for quick access.


Ready to rethink your Java?

Join Pwrteams

Check out our vacancies here