Prev Next

Java / Java 21 latest features Interview questions

Last updated

1. What is Java 21? 2. What are the final features delivered in Java 21? 3. What are Sequenced Collections? 4. What methods does SequencedCollection add? 5. What is a virtual thread? 6. How do you create virtual threads in Java 21? 7. What is pattern matching for switch? 8. What are record patterns? 9. What is the purpose of the when clause in switch? 10. What is Generational ZGC? 11. How do you enable Generational ZGC? 12. What are string templates in Java 21? 13. What are unnamed patterns and variables? 14. What are unnamed classes and instance main methods? 15. What are scoped values? 16. What is structured concurrency? 17. What is the Foreign Function and Memory API? 18. What is the Vector API in Java 21? 19. What is the Key Encapsulation Mechanism API? 20. What is a preview feature and how do you enable it? 21. What new methods were added to String and StringBuilder? 22. What does Math.clamp do? 23. What is the difference between platform threads and virtual threads? 24. Why shouldn't you pool virtual threads? 25. How does the virtual thread scheduler work? 26. What happens when a virtual thread is pinned? 27. Why don't virtual threads speed up CPU-bound work? 28. How do you limit concurrency with virtual threads? 29. How does ExecutorService change in Java 21? 30. How do you troubleshoot virtual thread pinning? 31. How do you take a thread dump with virtual threads? 32. Why should you be careful with ThreadLocal on virtual threads? 33. What is the difference between ScopedValue and ThreadLocal? 34. Explain the execution flow of StructuredTaskScope? 35. What is the difference between ShutdownOnFailure and ShutdownOnSuccess? 36. How does exhaustiveness checking work in a pattern switch? 37. How does a pattern switch handle null? 38. Why does pattern dominance cause compile errors? 39. Explain the execution flow of a native call using FFM? 40. How do nested record patterns work? 41. What is the difference between STR, FMT and RAW processors? 42. Why are string templates safer than concatenation? 43. What happened to Java 21 preview features in later releases? 44. What is the difference between Generational and non-generational ZGC? 45. When would you choose ZGC over G1? 46. What is the difference between reversed() and Collections.reverse()? 47. What is the difference between SequencedMap and SortedMap? 48. How do you enable virtual threads in Spring Boot? 49. What can break when upgrading to Java 21? 50. How do you migrate from Java 17 to Java 21?

1. What is Java 21?

Java 21 is the Java SE release that reached general availability in September 2023. It is a Long-Term Support (LTS) release and the successor to Java 17 as the LTS baseline.

It ships 15 JEPs. The headline ones are virtual threads, pattern matching for switch, record patterns, Sequenced Collections and Generational ZGC, all final. Others, such as string templates and structured concurrency, arrive as preview features.

Being LTS is why many teams standardise on it: vendors keep shipping security updates for years. Its class file major version is 65.

Take quiz
Java 21 is classified as:
a Long-Term Support (LTS) release
a short-lived feature release with six months of support
an incubator-only release
Which LTS release did Java 21 follow?
Java 11
Java 17
Java 20

2. What are the final features delivered in Java 21?

Eight JEPs became final (non-preview) in Java 21:

JEP Feature
431 Sequenced Collections
439 Generational ZGC
440 Record Patterns
441 Pattern Matching for switch
444 Virtual Threads
449 Deprecate the Windows 32-bit x86 Port for Removal
451 Prepare to Disallow the Dynamic Loading of Agents
452 Key Encapsulation Mechanism API

The other seven are not final: six are preview features and one (the Vector API) is still an incubator module.

Of these, virtual threads, pattern matching for switch and Sequenced Collections change day-to-day coding the most. The agent and Windows entries are mainly housekeeping for tool and platform maintainers.

Take quiz
Which of these was a final feature in Java 21?
Scoped values
Virtual threads
String templates
Structured concurrency
Of the 15 JEPs in Java 21, how many were preview or incubator?
None, all were final
Three
Seven (six preview, one incubator)
All fifteen

3. What are Sequenced Collections?

Sequenced Collections (JEP 431) add three interfaces: SequencedCollection, SequencedSet and SequencedMap. They describe collections that have a defined encounter order and give them one common API for first, last and reverse access.

Before Java 21, getting the last element meant list.get(list.size() - 1), deque.getLast() or an iterator loop, depending on the type. Now it is the same call everywhere.

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst();   // a
list.getLast();    // c
list.reversed();   // [c, b, a] - a view, not a copy

Take quiz
Which problem do Sequenced Collections solve?
HashMap is slow with many keys
No common API for first, last and reverse order across collection types
ArrayList is not thread-safe
Which interface models a Map with a defined encounter order?
SequencedMap
OrderedMap
IndexedMap
LinkedMapView

4. What methods does SequencedCollection add?

SequencedCollection adds seven methods on top of Collection:

Method Purpose
addFirst(e) / addLast(e) insert at either end
getFirst() / getLast() read either end
removeFirst() / removeLast() remove and return either end
reversed() reverse-ordered view

Reading or removing from an empty collection throws NoSuchElementException. Immutable collections, or ones that cannot support the operation, throw UnsupportedOperationException.

SequencedMap adds firstEntry(), lastEntry(), pollFirstEntry(), pollLastEntry(), putFirst(), putLast() and reversed(), plus sequenced views of keys, values and entries.

Take quiz
On an empty ArrayList, getFirst() throws:
IndexOutOfBoundsException
NoSuchElementException
NullPointerException
nothing, it returns null
Which method returns a reverse-ordered view of a sequenced collection?
reverse()
toReversed()
reversed()
flip()

5. What is a virtual thread?

A virtual thread (JEP 444) is a lightweight java.lang.Thread managed by the JVM rather than the operating system. Many virtual threads are multiplexed onto a small number of OS threads called carrier threads.

They are cheap enough to create one per task, even millions at a time. When a virtual thread blocks on I/O, it is unmounted from its carrier, so the OS thread can run other work.

They use the normal Thread API, are always daemon threads, and have a fixed normal priority. They target the thread-per-request style for blocking, I/O-heavy servers.

Take quiz
Virtual threads are scheduled by:
the operating system kernel directly
the garbage collector
the JVM, on top of a small set of carrier threads
Virtual threads are best suited to:
high-concurrency workloads that spend time blocking on I/O
long-running CPU-bound number crunching
replacing all JVM tuning

6. How do you create virtual threads in Java 21?

There are three common ways, all in the standard API (no preview flag needed):

// 1. Builder
Thread t = Thread.ofVirtual().name("worker-1").start(() -> work());

// 2. Shortcut
Thread.startVirtualThread(() -> work());

// 3. Executor, one new virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> work());
}

Use Thread.ofVirtual().unstarted(task) if you want to create the thread first and start it later. Thread.isVirtual() tells you which kind you have.

Virtual threads have no public constructor, so new Thread(...) always gives a platform thread. A ThreadFactory from Thread.ofVirtual().factory() is handy when a framework asks for one.

Take quiz
Which call starts a virtual thread directly?
new Thread(task).setVirtual(true)
Thread.startVirtualThread(task)
Executors.newFixedThreadPool(10_000)
Executors.newVirtualThreadPerTaskExecutor() creates:
a fixed pool of ten virtual threads
one reusable carrier thread
a new virtual thread for every submitted task

7. What is pattern matching for switch?

Pattern matching for switch (JEP 441, final in Java 21) lets case labels match on an object's type and bind a variable, instead of only matching constants.

static String describe(Object o) {
    return switch (o) {
        case Integer i -> "int " + i;
        case String s  -> "string of length " + s.length();
        case null      -> "null";
        default        -> "something else";
    };
}

It replaces long if (o instanceof ...) chains and works with sealed types, records, guards and null.

Take quiz
Pattern matching for switch lets case labels:
test an object's type and bind a variable
match only int constants
run each case in parallel
Pattern matching for switch became final in:
Java 21
Java 17
Java 19
Java 22

8. What are record patterns?

A record pattern (JEP 440) deconstructs a record into its components directly inside instanceof or switch, so you skip the accessor calls.

record Point(int x, int y) {}

if (obj instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

The pattern checks the type and, if it matches, binds x and y in one step. Patterns can also be nested, which is covered in a later question.

Record patterns pair well with sealed interfaces: a switch over the sealed type can deconstruct each record case and the compiler still checks that every case is covered.

Take quiz
A record pattern is used to:
serialize a record to JSON
create a new record instance
deconstruct a record into its components
Record patterns can appear in:
instanceof and switch
only annotation processors
only for-loop headers

9. What is the purpose of the when clause in switch?

The when clause adds a guard, an extra boolean condition, to a pattern label. The case matches only if the pattern matches and the guard is true.

return switch (o) {
    case Integer i when i > 100 -> "large";
    case Integer i               -> "small";
    default                      -> "not an int";
};

If the guard is false, evaluation simply moves to the next case. Order matters: the guarded label must come before the unguarded one for the same type.

Before Java 21's final syntax, preview versions used && for guards. The when keyword replaced it, so guards read clearly and cannot be confused with boolean operators inside the pattern.

Take quiz
Which keyword adds a condition to a case pattern?
where
when
if
In 'case String s when s.isEmpty()', if the guard is false:
a MatchException is thrown
the default branch runs immediately
evaluation continues with later cases

10. What is Generational ZGC?

Generational ZGC (JEP 439) extends the Z Garbage Collector with separate young and old generations. Young objects are collected often, old objects rarely.

Most objects die young, so this avoids repeatedly marking long-lived data. The result is fewer allocation stalls, lower CPU and memory overhead, and the same very short (sub-millisecond) pauses ZGC is known for.

It is a good fit for latency-sensitive services with high allocation rates.

You do not change application code to use it. It is a JVM option, so the benefit shows up as lower GC CPU usage and a smaller heap requirement for the same throughput.

Take quiz
Generational ZGC mainly improves ZGC by:
collecting young objects separately and more often
adding long stop-the-world compaction
removing concurrent marking
Which statement about ZGC pauses is true?
They grow in proportion to heap size
They stay very short, in the sub-millisecond range
Every collection is a full stop-the-world

11. How do you enable Generational ZGC?

In Java 21 you must opt in with two flags:

java -XX:+UseZGC -XX:+ZGenerational -jar app.jar

Using -XX:+UseZGC alone still gives the older non-generational ZGC. Generational mode became the default in a later release (Java 23), and the non-generational mode was removed after that.

To confirm what is running, start the JVM with -Xlog:gc and check the collector name in the log.

If you also set -Xmx, leave some free headroom. ZGC works concurrently with your application, and a heap that is nearly full forces it to stall allocating threads.

Take quiz
Which flags enable Generational ZGC in Java 21?
-XX:+UseZGC only, generational is default
-XX:+UseGenerationalGC
-XX:+UseG1GC -XX:+ZGenerational
-XX:+UseZGC -XX:+ZGenerational
In Java 21, -XX:+UseZGC on its own gives you:
the non-generational ZGC
Generational ZGC
G1 with extra tuning

12. What are string templates in Java 21?

String templates (JEP 430) embed expressions in a string literal with \{...}. A template processor then turns the template into a result. The built-in STR processor does plain interpolation.

String name = "Ana";
int a = 2, b = 3;
String s = STR."Hello \{name}, sum is \{a + b}";
// Hello Ana, sum is 5

In Java 21 this is a preview feature, so it needs --enable-preview. STR is imported automatically in every source file.

Compared with + concatenation or String.format, the template keeps the text and the values visible together, which makes long messages easier to read and review.

Take quiz
Embedded expressions in a string template are written as:
${expression}
\{expression}
#{expression}
{{expression}}
In Java 21, string templates are:
a fully final feature
an incubator module
a preview feature that needs --enable-preview

13. What are unnamed patterns and variables?

JEP 443 (preview in Java 21) lets you use the underscore _ for a variable or pattern you must declare but never use. The compiler then knows it is intentionally ignored.

try { parse(text); }
catch (NumberFormatException _) { log("bad input"); }

if (shape instanceof Point(int x, _)) { use(x); }

for (var _ : list) { count++; }

An unnamed variable declares nothing you can read later. An unnamed pattern matches anything without binding it.

Using _ also documents intent: a reader knows the value is deliberately ignored, and static analysis tools stop reporting an unused variable warning.

Take quiz
In the Java 21 preview, the underscore denotes:
an unnamed variable or pattern whose value is ignored
a wildcard import
a generic type placeholder
Unnamed variables are useful when:
you want to make a field static
a variable must be declared but is never used
a method needs to be overloaded

14. What are unnamed classes and instance main methods?

JEP 445 (preview in Java 21) shortens the first program a beginner writes. A source file may contain just methods and fields, with no class declaration, and main need not be public static or take String[] args.

// Hello.java
void main() {
    System.out.println("Hello, Java 21");
}

The compiler wraps this in an unnamed class. Run it with java --enable-preview --source 21 Hello.java. The aim is a gentler learning curve, not a change to how large applications are structured.

Take quiz
Under JEP 445, a minimal program can be written as:
only inside a public final class
only as static void run()
a top-level void main() with no class declaration
The main goal of this feature is to:
lower the entry barrier for beginners
replace packages
speed up garbage collection

15. What are scoped values?

Scoped values (JEP 446, preview in Java 21) let you share immutable data with methods you call, and with child threads, for a bounded stretch of execution. They are a lighter, safer alternative to ThreadLocal, especially with virtual threads.

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

ScopedValue.where(USER, "alice").run(() -> handle());

void handle() { System.out.println(USER.get()); }  // alice

Once run returns, the binding disappears automatically.

They are designed for request context such as the current user or a trace ID. Unlike ThreadLocal, nothing needs to be cleaned up, because the binding ends when the lambda returns.

Take quiz
Scoped values are:
mutable globals with setters
immutable and bound to a limited execution scope
thread pool settings
Calling get() on a scoped value that is not bound:
returns null
returns the last bound value
throws NoSuchElementException

16. What is structured concurrency?

Structured concurrency (JEP 453, preview in Java 21) treats a group of related tasks running in different threads as one unit of work. The main API is StructuredTaskScope.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<User>  user  = scope.fork(() -> findUser());
    Subtask<Order> order = scope.fork(() -> findOrder());

    scope.join().throwIfFailed();
    return new Result(user.get(), order.get());
}

Subtasks cannot outlive the scope, so there are no leaked threads, and failures or cancellation propagate cleanly.

It follows the same idea as structured programming: a block of code that starts concurrent work should also be the block that waits for it, which makes thread dumps and error handling easier to follow.

Take quiz
Structured concurrency guarantees that:
subtasks finish before the enclosing scope exits
tasks run only on the main thread
subtask exceptions are silently ignored
Which class is central to structured concurrency in Java 21?
TaskGroup
StructuredTaskScope
ConcurrentScope

17. What is the Foreign Function and Memory API?

The Foreign Function & Memory (FFM) API (JEP 442, third preview in Java 21) lets Java call native libraries and work with off-heap memory safely, without writing JNI glue code.

Its main pieces are Linker (calls native functions), MemorySegment (a bounds-checked memory region) and Arena (controls when memory is freed).

It aims to replace JNI and much of the risky sun.misc.Unsafe usage. In Java 21 it still needs --enable-preview.

Its safety checks include bounds checking on every segment access and temporal checks, so use-after-free on a closed arena throws an exception instead of crashing the JVM.

Take quiz
The FFM API aims to replace:
JDBC drivers
RMI
JNI and many uses of sun.misc.Unsafe
In the FFM API, native memory lifetime is controlled by:
an Arena
finalizers
System.gc()

18. What is the Vector API in Java 21?

The Vector API (JEP 448, sixth incubator in Java 21) lets you write SIMD computations in Java that the JIT compiler maps to CPU vector instructions, such as AVX or NEON, where the hardware supports them.

var sp = FloatVector.SPECIES_PREFERRED;
for (int i = 0; i < sp.loopBound(a.length); i += sp.length()) {
    FloatVector.fromArray(sp, a, i)
               .add(FloatVector.fromArray(sp, b, i))
               .intoArray(c, i);
}

It lives in the jdk.incubator.vector module, so you add --add-modules jdk.incubator.vector. It stays incubating until Project Valhalla features land.

Take quiz
The Vector API expresses:
database vector searches
SIMD computations that compile to CPU vector instructions
growable arrays like java.util.Vector
Which module must you add to use it?
java.vector
jdk.simd
jdk.incubator.vector

19. What is the Key Encapsulation Mechanism API?

The KEM API (JEP 452, final in Java 21) adds javax.crypto.KEM. A KEM uses a public key to securely establish a shared symmetric key. It is a building block for hybrid encryption and for post-quantum schemes.

  1. The receiver generates a key pair and shares the public key.
  2. The sender calls newEncapsulator(publicKey).encapsulate() and gets a secret key plus an encapsulation message.
  3. The receiver passes that message to newDecapsulator(privateKey).decapsulate(...) and recovers the same secret.

The JDK itself includes a DHKEM implementation, and other providers can plug in more algorithms.

Take quiz
A KEM is used to:
establish a shared symmetric key using public-key cryptography
compress network traffic
hash stored passwords
What does the sender transmit to the receiver in a KEM exchange?
the sender's private key
the encapsulation message
the hash salt

20. What is a preview feature and how do you enable it?

A preview feature is fully specified and implemented but not yet permanent, so the Java community can try it and give feedback before it is finalised. It may change, or even be dropped.

You must opt in at both compile time and run time, using the same JDK version:

javac --release 21 --enable-preview Main.java
java --enable-preview Main

Classes compiled this way are marked with a special minor class file version, so they run only on that exact Java release. That is why preview features are not meant for production libraries.

Take quiz
Which flag lets you run preview features?
--allow-preview
-XX:+UsePreview
--enable-preview
Classes compiled with Java 21 preview features run on:
only Java 21
any later JDK
any JDK from 8 upwards

21. What new methods were added to String and StringBuilder?

Java 21 added a handful of small but handy text methods:

  • StringBuilder.repeat(CharSequence, int) and repeat(int codePoint, int) append text several times.
  • String.indexOf(int ch, int from, int to) and indexOf(String, int, int) search within a range.
  • String.splitWithDelimiters(regex, limit) splits and keeps the matched delimiters.
  • Character.isEmoji() and related checks.
new StringBuilder().repeat("ab", 3);      // ababab
"a1b2c".splitWithDelimiters("\\d", 0);    // [a, 1, b, 2, c]

None of these change existing behaviour, so they are safe to adopt gradually. repeat in particular removes the common loop that appended the same text several times.

Take quiz
StringBuilder.repeat("ab", 3) produces:
ab3
aaabbb
abab
ababab
splitWithDelimiters differs from split because it:
ignores the regex
returns a single String
also returns the matched delimiters

22. What does Math.clamp do?

Math.clamp (new in Java 21) restricts a value to a range. It returns min if the value is below the range, max if above, and the value itself otherwise.

Math.clamp(150, 0, 100);   // 100
Math.clamp(-5, 0, 100);    // 0
Math.clamp(42, 0, 100);    // 42

There are overloads for long, double and float. If min is greater than max, it throws IllegalArgumentException. It replaces the old Math.max(min, Math.min(max, v)) idiom.

The long, int, int overload returns an int, so no cast is needed when clamping a long value into an int range, such as a page size or percentage.

Take quiz
Math.clamp(150, 0, 100) returns:
0
100
150
50
If min is greater than max, Math.clamp:
swaps the two bounds
throws IllegalArgumentException
returns max

23. What is the difference between platform threads and virtual threads?

A platform thread wraps one OS thread. A virtual thread is a JVM-managed thread that borrows an OS thread only while it is running.

Aspect Platform thread Virtual thread
Mapping 1:1 with an OS thread Many virtual threads share a few carrier threads
Stack Fixed OS stack, often around 1 MB reserved Small stack chunks on the heap that grow as needed
Practical count Thousands Millions
Creation cost High, so pooled Very low, so one per task
When blocking Blocks the OS thread Unmounts and frees the carrier
Scheduler Operating system JVM (ForkJoinPool)
Daemon / priority Configurable Always daemon, fixed normal priority

The API is the same, so existing blocking code usually works unchanged.

Take quiz
When a virtual thread blocks on I/O, typically:
its carrier thread blocks as well
the JVM terminates it
it unmounts and frees its carrier thread
The priority of a virtual thread is:
fixed at normal priority
adjustable from 1 to 10
inherited from its carrier thread

24. Why shouldn't you pool virtual threads?

Pools exist because platform threads are expensive: you create a few and reuse them. Virtual threads are cheap, so the intended model is one new virtual thread per task.

Pooling them gives no benefit and causes harm:

  1. It adds queueing and contention for a resource that is not scarce.
  2. A long-lived pooled thread can leak ThreadLocal state between unrelated tasks.
  3. The pool size becomes an accidental concurrency limit.

If you really need to cap load on a database or remote API, limit access to that resource with a Semaphore instead of limiting threads.

A good test is this: if you find yourself writing newFixedThreadPool(n) for virtual threads, you are almost certainly trying to limit something else, and a Semaphore says that more directly.

Take quiz
Pooling virtual threads is discouraged because:
the JVM cannot reuse them safely
they are cheap, so one per task is the intended model
pools reject Runnable tasks
To cap load on a database when using virtual threads, use:
a fixed pool of virtual threads
Thread.setPriority(1)
a Semaphore around the database call

25. How does the virtual thread scheduler work?

Virtual threads run as continuations on a dedicated ForkJoinPool operating in FIFO mode. Its threads are the carrier threads, and their number defaults to the CPU count.

When code hits a blocking operation such as a socket read, sleep or a lock wait, the continuation yields. Its stack frames are copied to the heap, and the carrier picks up another virtual thread. When the operation completes, the virtual thread is resubmitted and may resume on a different carrier.

flowchart LR
  A["Virtual thread starts"] --> B["Mounted on carrier thread"]
  B --> C{"Blocking call?"}
  C -- No --> D["Runs to completion"]
  C -- Yes --> E["Unmount: stack copied to heap"]
  E --> F["Carrier runs another virtual thread"]
  E --> G["I/O ready: resubmitted to scheduler"]
  G --> B

Tune it with -Djdk.virtualThreadScheduler.parallelism and -Djdk.virtualThreadScheduler.maxPoolSize, though the defaults are usually right.

Take quiz
The default virtual thread scheduler is based on:
a ForkJoinPool
a cached pool of platform threads
OS-level fibers
Where are a parked virtual thread's stack frames kept?
on the carrier's native stack
on the heap
in a file on disk

26. What happens when a virtual thread is pinned?

A virtual thread is pinned when it cannot unmount from its carrier. In Java 21 this happens when it blocks while inside a synchronized block or method, or while a native or foreign frame is on its stack.

While pinned, a blocking call holds the OS carrier thread hostage. A few pinned threads reduce throughput. If every carrier is pinned, other virtual threads starve.

synchronized (lock) {
    socket.read();     // pinned: carrier is blocked too
}

lock.lock();           // ReentrantLock: can unmount while waiting
try { socket.read(); } finally { lock.unlock(); }

Fix it by using ReentrantLock around blocking work and keeping synchronized sections short. Java 24 later removed pinning for synchronized (JEP 491).

Take quiz
In Java 21, a virtual thread blocking inside synchronized:
is terminated by the JVM
releases the carrier as usual
stays pinned to its carrier thread
A recommended fix for pinning is to:
use ReentrantLock around blocking sections
raise the thread priority
call System.gc() before blocking

27. Why don't virtual threads speed up CPU-bound work?

Virtual threads improve concurrency, not parallelism. The number of carrier threads is about the number of CPU cores, so only that many virtual threads can execute at the same instant.

A CPU-bound task never blocks, so it never unmounts and never frees its carrier for others. A million such tasks still queue behind a handful of cores, and you add scheduling overhead for no gain.

Virtual threads help when tasks spend most of their time waiting. By Little's law, more requests can be in flight at once if waiting no longer ties up an OS thread. For heavy computation, use a fixed pool sized to the cores, the ForkJoin framework or parallel streams.

Take quiz
Why is there no speedup for CPU-bound work?
virtual threads cannot run computations
parallelism is still capped by the number of CPU cores
they only run after garbage collection
Virtual threads help most when tasks:
do heavy matrix math
never touch the network
spend most of their time waiting

28. How do you limit concurrency with virtual threads?

Do not limit the thread count. Limit the resource the threads use, with a Semaphore. This keeps the one-thread-per-task model while protecting a database, API or file system.

Semaphore permits = new Semaphore(50);

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (var job : jobs) {
        executor.submit(() -> {
            permits.acquire();
            try { return callDatabase(job); }
            finally { permits.release(); }
        });
    }
}

At most 50 tasks hit the database at once, while the rest wait cheaply as parked virtual threads. Always release in finally.

Pick the permit count from the downstream limit, such as the database connection pool size, not from the CPU count. Waiting on acquire() is cheap because the virtual thread simply parks.

Take quiz
A Semaphore with 50 permits limits:
how many tasks use the resource at once
how many virtual threads can exist
the number of carrier threads
Which class bounds concurrent access to a resource?
ThreadLocal
Semaphore
CountDownLatch

29. How does ExecutorService change in Java 21?

In Java 21, ExecutorService extends AutoCloseable, so you can use it in try-with-resources. Its close() method initiates an orderly shutdown and then waits for submitted tasks to finish.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        }));
}   // close() blocks here until all tasks complete

Before this, you had to call shutdown() and awaitTermination() yourself, and forgetting them was a common source of leaks. If the waiting thread is interrupted, close() calls shutdownNow().

Executors.newVirtualThreadPerTaskExecutor() combined with try-with-resources gives a clear scope: tasks start inside the block, and the block ends only when all of them are done.

Take quiz
ExecutorService.close() in Java 21:
kills running tasks immediately
only frees memory
initiates shutdown and waits for submitted tasks to finish
ExecutorService in Java 21 can be used in:
try-with-resources
only static initializers
only with platform threads

30. How do you troubleshoot virtual thread pinning?

Work through these steps:

  1. Run with -Djdk.tracePinnedThreads=full. The JVM prints a stack trace whenever a virtual thread blocks while pinned, with the pinning frames marked.
  2. Record with Java Flight Recorder and look at the jdk.VirtualThreadPinned event, which fires when a pinned thread blocks longer than a threshold (20 ms by default).
  3. Find the synchronized block or native call in the trace.
  4. Replace it with ReentrantLock, or shorten the section so it no longer wraps blocking I/O.
  5. Check third-party libraries and JDBC drivers, which are common culprits, and upgrade them.

Short pinned sections around fast code are harmless. Focus on pinning that wraps long blocking calls.

Take quiz
Which system property prints stack traces of pinned threads?
jdk.virtualThreadDebug
jdk.tracePinnedThreads
java.pinned.trace
Which JFR event reports virtual thread pinning?
jdk.CarrierBlocked
jdk.ThreadPark
jdk.VirtualThreadPinned

31. How do you take a thread dump with virtual threads?

The classic jstack and jcmd <pid> Thread.print output lists only platform threads, because an app may hold millions of virtual threads.

Use the newer command instead:

jcmd <pid> Thread.dump_to_file -format=json threads.json

This writes all threads, including virtual ones, and the JSON format groups threads in a structured-concurrency tree so related tasks are easy to spot. Plain text output is the default if you omit -format.

You can also call HotSpotDiagnosticMXBean.dumpThreads from inside the application.

For a quick look at live problems, a JSON dump can be post-processed with jq or loaded into tools that understand it, which is far easier than scanning a text file with thousands of stacks.

Take quiz
Which command includes virtual threads in a dump?
jcmd <pid> Thread.dump_to_file
jstack -v
jmap -histo
Why does jcmd Thread.print miss virtual threads?
virtual threads have no stack
there may be millions, so it lists only platform threads
they are hidden by the security manager

32. Why should you be careful with ThreadLocal on virtual threads?

Virtual threads do support ThreadLocal, but the usual patterns stop paying off.

  1. Memory multiplies. Each virtual thread gets its own copy. With a million threads, a per-thread cache of a heavy object becomes a million objects.
  2. Caching assumptions break. Code that stored an expensive SimpleDateFormat per pooled thread now rebuilds it for every short-lived thread.
  3. Inheritance costs. InheritableThreadLocal copies values into every child thread.

Prefer thread-safe shared objects, pass data as parameters, or use ScopedValue for immutable per-request context.

If you keep ThreadLocals for legacy libraries, call remove() when the task finishes. Short-lived virtual threads reduce the leak risk, but a long-lived one can still hold stale data.

Take quiz
Why can ThreadLocal caching hurt with virtual threads?
virtual threads do not support ThreadLocal
values are shared across all threads
each short-lived thread creates its own copy, multiplying memory use
A better option for sharing immutable request context is:
ScopedValue
a static mutable map
System properties

33. What is the difference between ScopedValue and ThreadLocal?

Both carry per-thread context, but they behave very differently.

Aspect ThreadLocal ScopedValue (preview in 21)
Mutability set() any time Immutable within a binding
Lifetime Until remove() or thread end Only while run() or call() executes
Child threads InheritableThreadLocal copies values Shared with StructuredTaskScope children, no copy
Memory cost A copy per thread Lightweight, no per-thread map to clean
Leak risk Yes, if not removed None, the binding ends automatically

Rebinding is allowed only by nesting another where(...), and the outer value returns when the inner scope ends. That makes data flow easier to reason about.

In short: use ScopedValue for read-only context that flows down a call tree, and keep ThreadLocal only where you truly need a mutable per-thread slot.

Take quiz
What is the main difference in mutability?
ScopedValue allows set() anywhere, ThreadLocal is immutable
ScopedValue is immutable within its binding, while ThreadLocal can be reassigned
there is no difference
A scoped value binding ends when:
the thread terminates
garbage collection runs
the run or call operation returns

34. Explain the execution flow of StructuredTaskScope?

A StructuredTaskScope runs in a fixed order, enforced by try-with-resources:

  1. The owner thread opens the scope.
  2. Each fork() starts a new virtual thread running one subtask.
  3. The owner calls join(), which waits until all subtasks finish, or until the policy calls shutdown().
  4. The owner reads results with Subtask.get(). Calling it before join() throws IllegalStateException.
  5. close() runs at the end of the block and guarantees no subtask thread is still alive.
sequenceDiagram
  participant M as Owner thread
  participant S as StructuredTaskScope
  participant T1 as Subtask 1
  participant T2 as Subtask 2
  M->>S: open scope
  M->>S: fork task 1
  S->>T1: start virtual thread
  M->>S: fork task 2
  S->>T2: start virtual thread
  M->>S: join
  T1-->>S: result
  T2-->>S: result
  S-->>M: join returns
  M->>S: close

If one subtask fails under ShutdownOnFailure, the scope cancels the others, so join() returns early.

Take quiz
What must be called before Subtask.get()?
shutdown()
join()
fork() a second time
commit()
Each fork() in a default scope starts:
a task on the common ForkJoinPool
a new virtual thread
a platform thread from a fixed pool

35. What is the difference between ShutdownOnFailure and ShutdownOnSuccess?

Both are StructuredTaskScope policies. They differ in when they cancel the remaining subtasks and what you read afterwards.

Aspect ShutdownOnFailure ShutdownOnSuccess
Cancels others when the first subtask fails the first subtask succeeds
Typical use Every subtask result is required (aggregate) Any one result is enough (race, redundancy)
Reading the outcome join().throwIfFailed(), then Subtask.get() join().result() returns the first success
If it all goes wrong Rethrows the first exception result() throws if no subtask succeeded

Use ShutdownOnFailure to load a user, orders and preferences for one page. Use ShutdownOnSuccess to query three mirror services and take whichever replies first. You can also extend StructuredTaskScope and override handleComplete for a custom policy.

Take quiz
To query three mirrors and use the fastest reply, use:
ShutdownOnFailure
a plain invokeAll call
ShutdownOnSuccess
ShutdownOnFailure is suited when:
every subtask result is required
only the first result matters
tasks can never fail

36. How does exhaustiveness checking work in a pattern switch?

A switch expression, or a pattern switch statement, must cover every possible input. With a sealed hierarchy the compiler knows all permitted subtypes, so a default branch is unnecessary.

sealed interface Shape permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
        case Rect r   -> r.w() * r.h();
    };
}

If you add a fourth permitted type and recompile, this switch fails to compile until you handle it. That is the main benefit: new cases cannot be silently forgotten.

If the hierarchy changes after compilation (separately compiled classes), the switch throws MatchException at run time.

Take quiz
Add a new permitted subtype, recompile, and no default exists. What happens?
it silently runs the first case
a compile error for a non-exhaustive switch
a ClassCastException at runtime
Which exception appears at run time if a separately compiled hierarchy changes?
IllegalStateException
InstantiationError
MatchException
ArrayStoreException

37. How does a pattern switch handle null?

A classic switch throws NullPointerException if the selector is null. Java 21 keeps that default, but lets you handle it explicitly with case null.

String label = switch (obj) {
    case null      -> "nothing";
    case String s  -> "text: " + s;
    default        -> "other";
};

// or combine with default
switch (obj) {
    case String s       -> use(s);
    case null, default  -> fallback();
}

If there is no case null, a null selector still throws NullPointerException, so existing code keeps its behaviour. Handling null in the switch removes a separate guard before it.

This also helps with Optional-free APIs that return null: the null branch lives next to the other cases, so reviewers can see every outcome in one place.

Take quiz
Switching on null when no case null exists:
throws NullPointerException
jumps to default
returns null
Which label handles both null and unmatched values?
default null
case null, default
case default null

38. Why does pattern dominance cause compile errors?

A pattern label is dominated if an earlier label already matches every value it could match. That later case could never run, so the compiler rejects it.

switch (o) {
    case CharSequence cs -> use(cs);
    case String s        -> use(s);   // error: dominated by CharSequence
    default              -> other();
}

Every String is a CharSequence, so the first label wins. Swap them and the code compiles.

The same rule covers guards: put case Integer i when i > 0 before a plain case Integer i. Constant labels after a pattern that already covers them are also flagged. The rule turns subtle unreachable-code bugs into build errors.

Take quiz
Which ordering compiles?
case CharSequence cs first, then case String s
both orderings compile
case String s first, then case CharSequence cs
The goal of the dominance check is to:
prevent unreachable case labels
speed up JIT compilation
enforce variable naming

39. Explain the execution flow of a native call using FFM?

Calling a C function through the FFM API follows a fixed sequence. This example calls strlen (Java 21 preview API):

flowchart TD
  A["Open an Arena"] --> B["Get native Linker"]
  B --> C["Find symbol with SymbolLookup"]
  C --> D["Describe signature with FunctionDescriptor"]
  D --> E["Create downcall MethodHandle"]
  E --> F["Allocate MemorySegment in Arena"]
  F --> G["Invoke handle"]
  G --> H["Close Arena: memory freed"]
try (Arena arena = Arena.ofConfined()) {
    Linker linker = Linker.nativeLinker();
    MethodHandle strlen = linker.downcallHandle(
        linker.defaultLookup().find("strlen").orElseThrow(),
        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));

    MemorySegment str = arena.allocateUtf8String("Hello");
    long len = (long) strlen.invokeExact(str);   // 5
}

The arena owns the native memory and frees it deterministically on close. Use --enable-preview and --enable-native-access=ALL-UNNAMED to avoid warnings.

Take quiz
Which object finds a native symbol such as strlen?
an Arena
a SymbolLookup, for example linker.defaultLookup()
a MemoryLayout
With Arena.ofConfined(), the native memory is freed:
at the next garbage collection
never, it must be freed by C code
when the arena is closed

40. How do nested record patterns work?

A record pattern's components can themselves be patterns, including other record patterns. Java matches them recursively, left to right, and binds every variable only if the whole pattern matches.

record Point(int x, int y) {}
record Line(Point from, Point to) {}

if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
    double len = Math.hypot(x2 - x1, y2 - y1);
}

var lets the compiler infer each component type. This replaces chains of casts and accessor calls with one declarative test.

One detail: a nested type pattern such as Point p matches a null component, but a nested record pattern such as Point(var x, var y) does not. Combined with sealed interfaces, nested patterns make a switch over a data hierarchy compact and exhaustive.

Take quiz
Inside a record pattern, var:
infers the component's type
makes the component mutable
matches only null
Nested record patterns let you:
mutate immutable records
extract deeply nested values in a single test
skip type checking

41. What is the difference between STR, FMT and RAW processors?

All three are template processors for string templates (preview in Java 21).

Processor Result Notes
STR String Plain interpolation; imported automatically in every source file
FMT String Supports format specifiers before each expression; needs import static java.util.FormatProcessor.FMT;
RAW StringTemplate Not processed yet; you call process() on a processor later

int x = 7;
STR."x = \{x}";            // x = 7
FMT."x = %05d\{x}";        // x = 00007
var raw = RAW."x = \{x}";  // a StringTemplate object
String s = STR.process(raw);

Because RAW keeps the fragments and values separate, it is the basis for custom processors.

Most code only needs STR. Reach for FMT when you need aligned numbers or padding, and for RAW only when writing your own processor.

Take quiz
Which processor applies format specifiers like %05d?
STR
RAW
FMT
RAW returns:
a StringTemplate object
a String
a char array

42. Why are string templates safer than concatenation?

With concatenation, values are glued into one string before anything can inspect them. With a template, the processor receives the literal fragments and the embedded values separately, so it can validate, escape or parameterise them.

That is how a custom processor can build a safe SQL PreparedStatement or JSON object from user input, so values become parameters instead of text that could inject extra SQL.

var INSPECT = StringTemplate.Processor.of(
    st -> st.fragments() + " | " + st.values());

INSPECT."id=\{userId}";   // [id=, ] | [42]

A processor is not limited to returning String. It can return any type. Safety depends on the processor you pick: STR is plain interpolation and offers no extra protection.

Take quiz
Why can templates be safer than concatenation?
template strings are encrypted
a processor sees fragments and values separately, so it can validate or escape them
the compiler deletes user input
A custom template processor may return:
only String
only int
any type, not only String

43. What happened to Java 21 preview features in later releases?

Java 21 was a snapshot in time. Here is how its preview, incubator and final features developed (as of Java 25):

Feature Later outcome
Unnamed patterns and variables Final in Java 22 (JEP 456)
FFM API Final in Java 22 (JEP 454)
Scoped values Final in Java 25 (JEP 506)
Structured concurrency Still in preview in Java 25
Unnamed classes / instance main Reworked as implicit classes, then final in Java 25 as compact source files
String templates Second preview in Java 22, then removed in Java 23 for redesign
Generational ZGC Default in Java 23, non-generational mode removed in Java 24
Virtual threads Pinning for synchronized removed in Java 24 (JEP 491)
Vector API Still incubating

The practical lesson: avoid building production code on preview APIs, since names and behaviour can change between releases.

Take quiz
After Java 21, string templates:
were removed in Java 23 so they could be redesigned
became final in Java 22
moved into an incubator module
Unnamed patterns and variables became final in:
Java 21
Java 22
Java 24

44. What is the difference between Generational and non-generational ZGC?

Non-generational ZGC treats the whole heap as one region and marks all of it every cycle. Generational ZGC splits it into young and old generations.

Aspect Non-generational ZGC Generational ZGC
Collection scope Whole heap every cycle Young often, old rarely
Allocation stalls More likely under heavy allocation Much less likely
Memory overhead Higher headroom needed Lower
CPU cost Repeatedly scans long-lived objects Mostly avoids rescanning old data
Barriers Load barriers Load and store barriers

Both keep pauses very short. The generational design simply exploits the fact that most objects die young, so the collector does less work per byte allocated. Workloads that allocate many short-lived objects gain most.

Take quiz
Which workload benefits most from Generational ZGC?
programs that never allocate
heaps smaller than 100 MB only
allocation-heavy code creating many short-lived objects
The biggest design change in Generational ZGC is:
separate young and old generations
stop-the-world full compaction
reference counting instead of marking

45. When would you choose ZGC over G1?

Choose ZGC when tail latency matters more than raw throughput, especially on large heaps. G1, the default collector, balances throughput and pause time with a pause target of about 200 ms.

Factor G1 (default) ZGC
Pause times Tens to hundreds of ms Sub-millisecond, independent of heap size
Heap size Small to medium Multi-GB up to terabytes
Throughput Generally higher Slightly lower, more CPU and memory headroom
Best for General services, batch jobs Latency-sensitive APIs, trading, large caches

In Java 21, add -XX:+ZGenerational to get the better allocation behaviour. Measure with your real load before switching, and make sure you have spare memory, since ZGC works best with some headroom.

Take quiz
ZGC fits best when you need:
maximum throughput on a 256 MB batch job
very low pause times on a large heap
a single-core container
The default collector on a typical Java 21 server is:
ZGC
Epsilon
G1

46. What is the difference between reversed() and Collections.reverse()?

Collections.reverse(list) mutates the list in place and returns nothing. list.reversed() (Java 21) returns a view in reverse order and leaves the original untouched.

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3));

List<Integer> rev = nums.reversed();   // [3, 2, 1], O(1) view
nums.add(4);
System.out.println(rev);              // [4, 3, 2, 1] - view reflects change

Collections.reverse(nums);            // nums is now [4, 3, 2, 1] in place

Because the view is backed by the original, writes go through both ways. Wrap it in new ArrayList<>(...) if you need an independent copy. Calling reversed() on an immutable list gives an unmodifiable view, while Collections.reverse would throw.

Take quiz
list.reversed() returns:
a reverse-ordered view backed by the original list
a new copied list
void, after reversing in place
If you add to the original list after calling reversed(), the view:
stays stale
reflects the change
always throws ConcurrentModificationException

47. What is the difference between SequencedMap and SortedMap?

SortedMap orders entries by key comparison (natural order or a comparator). SequencedMap only promises a defined encounter order, such as insertion order. In Java 21, SortedMap extends SequencedMap.

Aspect SequencedMap SortedMap
Order comes from Insertion or access order Key comparison
Implementations LinkedHashMap, and sorted maps TreeMap, ConcurrentSkipListMap
putFirst / putLast Supported by LinkedHashMap Throw UnsupportedOperationException
firstEntry / lastEntry Available Available, plus firstKey, subMap and others

Sorted maps cannot honour putFirst because position is decided by the key, not by the caller. HashMap is not sequenced at all, as its iteration order is unspecified.

For caches that need least-recently-used ordering, LinkedHashMap in access-order mode is a SequencedMap, and pollFirstEntry() now gives you the eldest entry directly.

Take quiz
Which map supports putFirst()?
TreeMap
HashMap
LinkedHashMap
Calling putLast(k, v) on a TreeMap:
throws UnsupportedOperationException
appends it regardless of key order
replaces the comparator

48. How do you enable virtual threads in Spring Boot?

With Spring Boot 3.2 or later running on Java 21, set one property:

spring.threads.virtual.enabled=true

Spring Boot then runs embedded Tomcat or Jetty request handling, @Async tasks and scheduling on virtual threads, so each request gets its own cheap thread.

Check these before going live:

  1. The connection pool size, for example HikariCP, still caps database concurrency, so tune it.
  2. Libraries that block inside synchronized pin carriers in Java 21. Trace with -Djdk.tracePinnedThreads=full.
  3. Drop custom fixed thread pools that were only there to protect blocking calls.

Measure before and after with a load test. Gains are largest for services that wait on databases or HTTP calls, and negligible for CPU-heavy endpoints.

Take quiz
Which property enables virtual threads in Spring Boot 3.2?
spring.virtual-threads=on
spring.threads.virtual.enabled=true
server.tomcat.virtual=true
After enabling virtual threads, database concurrency is still capped by:
the number of carrier threads
the heap size
the connection pool size

49. What can break when upgrading to Java 21?

Most apps upgrade smoothly, but watch these areas:

  1. New default methods. getFirst(), getLast() and reversed() were added to List and Deque. A custom class implementing both can fail to compile because the inherited defaults conflict.
  2. Dynamic agent loading. Attaching an agent at run time, as some mocking and profiling tools do, now prints a warning. Use -javaagent at startup or add -XX:+EnableDynamicAgentLoading.
  3. Class file version 65. Bytecode tools such as ASM, Byte Buddy, JaCoCo and Lombok must be recent enough to read it.
  4. Encapsulated internals. Reflection into JDK internals needs --add-opens.
  5. Platform changes. The Windows 32-bit x86 port is deprecated for removal.

Run your full test suite and read the startup warnings before shipping.

Take quiz
Why might a class implementing both List and Deque fail to compile on 21?
both gained conflicting default methods such as reversed()
List became final
Deque was removed from the JDK
Which flag allows dynamic agent loading without a warning?
-XX:+AllowAgents
-XX:+EnableDynamicAgentLoading
--enable-agents

50. How do you migrate from Java 17 to Java 21?

Treat it as a staged upgrade, not a big-bang rewrite:

  1. Update tooling. Use recent versions of Maven or Gradle, their compiler plugins and your CI images with JDK 21.
  2. Update dependencies. Bump bytecode-dependent libraries (ASM, Byte Buddy, Lombok, JaCoCo, Mockito) so they handle class file version 65.
  3. Build and test on 21 with the same source level first. Read warnings about agents and reflection.
  4. Review JVM flags. Remove obsolete options and re-test GC choices.
  5. Adopt features gradually. Start with getFirst()/getLast(), pattern switch and record patterns, then try virtual threads on I/O-heavy services.
  6. Keep preview features off in production code.

Verify performance and memory in a staging environment before the final rollout.

Take quiz
What should come before adopting Java 21 features?
rewriting all code with virtual threads
enabling every preview feature
running the existing test suite on JDK 21 with updated build tooling and libraries
Which libraries typically need updates for class file version 65?
bytecode libraries such as ASM and Byte Buddy
properties file parsers
CSS minifiers
«
»

Comments & Discussions