eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Introduction

With Virtual threads, we can scale application performance for any I/O-intensive workflows. By contrast, with the platform threads, we had to carefully manage the expensive operating system resources and utilize non-blocking I/O, which involved complicated asynchronous code like CompletableFuture.

Even though virtual threads solve the scarcity problem with limited operating system threads, certain scenarios can cause platform thread blocking.

In this tutorial, we’ll learn different pinning scenarios with an example code. We’ll also debug and implement the fix for one such example. We’ll also learn the scenarios resolved in JDK 24.

2. Virtual Thread Pinning Scenarios

Virtual threads are a short-lived, lightweight thread construct that gets mounted onto the platform “carrier” thread by the JVM scheduler. It then executes the task and unmounts from the platform thread. The platform thread becomes available for the next task.

However, there are situations where the virtual thread and the corresponding carrier thread get blocked for a longer duration.

While this does not affect the business logic, it hampers the application’s scalability. A few common reasons are running heavy CPU-related tasks, waiting while holding a lock, or being blocked within a native method execution.

Though we should avoid using the virtual threads for any CPU-intensive operation. We’ll still need to understand other scenarios.

2.1. Synchronized Method or Block

Let’s imagine a real-world example of adding products to the shopping cart.

We’ll implement the CartService class with an update method with the productId as a lock:

public class CartService {

    private final Map<String, Integer> products;
    private final Map<String, Object> locks = new ConcurrentHashMap<>();

    public void update(String productId, int quantity) {
        Object lock = locks.computeIfAbsent(productId, k -> new Object());

        synchronized (lock) {
            simulateAPI();
            products.merge(productId, quantity, Integer::sum);
        }

        LOGGER.info("Updated Cart for {} {}", productId, quantity);
    }
}

In the above code, we’re calling a simulated API instead of an actual one for demonstration purposes.

We’ll simulate the downstream API call with a thread sleep method:

private void simulateAPI() {
    try {
        Thread.sleep(50);
    } catch (InterruptedException ex) {
        throw new RuntimeException(ex);
    }
}

Next, we’ll try to debug the above code.

2.2. Debug Pinning

Let’s test the above code using the Java flight recorder.

We’ll verify the pinning by running the CartService update method inside a virtual thread and assert the JFR events:

@Test
void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception {
    Path file = Path.of("pinning.jfr");

    try (Recording recording = new Recording()) {
        recording.enable("jdk.VirtualThreadPinned")
          .withThreshold(Duration.ofMillis(1));
        recording.start();

        Thread th = Thread.ofVirtual().start(() ->
            cartService.update("test1", 2));

        th.join();
        recording.stop();
        recording.dump(file);
}

    try (RecordingFile rf = new RecordingFile(file)) {
        assertTrue(rf.hasMoreEvents());

        while (rf.hasMoreEvents()) {
            RecordedEvent event = rf.readEvent();

            System.out.println(event);
            assertEquals("jdk.VirtualThreadPinned", event.getEventType().getName());
            assertEquals("Virtual Thread Pinned", event.getEventType().getLabel());
        }
    }

    Files.delete(file);
}

In the above test, we assert that the RecordedEvent includes the jdk.VirtualThreadPinned event.
Also, we can see the jdk.VirtualThreadPinned event in the console log:

jdk.VirtualThreadPinned {
  startTime = 13:28:30.738 (2026-03-29)
  duration = 101 ms
  eventThread = "" (javaThreadId = 32, virtual)
}

Additionally, we can view the pinned event in the JDK Mission Control dashboard:

Vortua;_Thread_Pinned_Image

Alternatively, we can detect pinning using the -Djdk.tracePinnedThreads=full VM flag:

VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1 reason:MONITOR
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:635)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:807)
    java.base/java.lang.Thread.sleep(Thread.java:507)
    com.baeldung.virtualthread.synchronize.CartService.simulateAPI(CartService.java:35)
    com.baeldung.virtualthread.synchronize.CartService.update(CartService.java:22) <== monitors:1

The above logs confirm that the platform thread is blocked.

Additionally, any code with Method-level synchronization, synchronization with wait, or the LockSupport.park method causes the pinning.

2.3. Native Method

The pinning can also occur in the native method calls. The native method can block either for any I/O operation or callbacks to Java code, which in turn blocks on a monitor.

We’ll implement a simple native method:

public class NativeDemo {

    static {
        System.loadLibrary("native-lib");
    }

    public native String nativeCall();
}

The reason for the pinning is that the thread has no control on native stack and needs to wait for the native method call to return.

Similarly, we can expect the same when running the native function using the Foreign Function API:

public void execute() {
    LOGGER.info("Running foreign function sleep...");

    Linker linker = Linker.nativeLinker();
    SymbolLookup stdlib = linker.defaultLookup();
    MethodHandle sleep = linker.downcallHandle(stdlib.find("sleep")
      .orElseThrow(), FunctionDescriptor.of(JAVA_INT, JAVA_LONG));

    try {
        sleep.invoke(100);
    } catch (Throwable ex) {
        throw new RuntimeException(ex);
    }
}

In the above code, the foreign function sleep method is defined by first creating the Linker object and then including the downcall MethodHandle instance.

As of JDK 21/24, JFR does not emit the jdk.VirtualThreadPinned event for blocking that occurs within native or FFM code.

Still, we can confirm the pinning by analyzing the thread dump and its impact on other virtual threads.

2.4. Static Initializer Block

Let’s imagine a class with a static initializer block.

We’ll implement a static initializer block with a Thread.sleep method to block the platform thread:

public class HeavyClass {

    static {
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex) {
            throw new RuntimeException(ex);
        }

        LOGGER.info("static initialization done");
    }
}

In the above code, we expect the virtual thread to get pinned as the static initializer holds an intrinsic lock.

2.5. Custom Class Loader

We’ll implement a custom class loader to verify virtual thread pinning in the above scenario.

First, let’s implement a CustomClassLoader class by extending the ClassLoader class and overriding the findClass method:

class CustomClassLoader extends ClassLoader {

    private final Path classDir;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        LOGGER.info("Finding class for {}", name);

        try {
            Path file = classDir.resolve(name.replace('.', '/') + ".class");
            byte[] bytes = java.nio.file.Files.readAllBytes(file);

            Thread.sleep(100);

            return defineClass(name, bytes, 0, bytes.length);
        } catch (InterruptedException | IOException ex) {
            LOGGER.error("Error while finding class file {}", ex.getMessage());
            throw new ClassNotFoundException(ex.getMessage(), ex);
        }
    }
}

Then, we’ll need to implement the loadClass method to override the system classloader:

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    LOGGER.info("Load class for {}", name);

    Class<?> clazz = findLoadedClass(name);

    if (clazz == null) {
        try {
            clazz = findClass(name);
        } catch (ClassNotFoundException ex) {
            clazz = super.loadClass(name, resolve);
        }
    }

    if (resolve) {
        resolveClass(clazz);
    }

    return clazz;
}

We’ll run a similar test for this class loader with the JFR recording enabled and verify the pinned thread event.

This is also true for any virtual thread that uses the same class while another thread initializes it.

3. Implement Synchronization Without Pinning

Although the JVM tries to compensate for the pinning by temporarily increasing the parallelism of the virtual thread scheduler, subjected to the default maximum of 256.
Still, we can overcome this with Java’s advanced concurrency control support.

We can use the Loom-aware locking mechanism, such as the ReentrantLock or ReadWriteLock class. The lock implementation within the java.util.concurrent.locks package does not cause the pinning.

We’ll implement the above CartService’s update method using the ReentrantLock lock:

public void update(String productId, int quantity) {
    Lock lock = locks.computeIfAbsent(productId, k -> new ReentrantLock());

    try {
        if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
            try {
                simulateAPI();
                products.merge(productId, quantity, Integer::sum);
            } finally{
                lock.unlock();
            }
            LOGGER.info("Updated Cart for {} {}", productId, quantity);
        }
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

If we run the earlier test again with the fixed version, we don’t observe any thread pinning event and blocking in the other threads.

Another approach to resolving the unnecessary pinning is to upgrade to JDK 24 or newer versions, where developers have fixed the issue.
We recommend upgrading the Java version if the upgrading effort is significantly smaller than reimplementing the code across services.

4. Pinning Scenarios Resolved in JDK 24

As part of JEP-491, the pinning issue is resolved for the synchronize method and block.
The Java team changed the implementation of the synchronized keyword, and now the virtual thread can acquire, hold, and release the lock independently of its carrier thread.

The system still expects pinning in the native method, class loader, and class initializer.

5. Benchmark

We’ll implement a JMH benchmark test for the earlier CartService update method with the AverageTime and Throughput modes:

@BenchmarkMode({ Mode.AverageTime, Mode.Throughput })
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Fork(value = 2)
@State(Scope.Benchmark)
public class BenchmarkVirtualThread {

    private final CartService cartService = new CartService();

    @Param({"100", "1000", "10000"})
    private int CONCURRENCY;

    @Benchmark
    public void benchmark() throws InterruptedException, IOException {
        List<Thread> threads = new ArrayList<>();
        IntStream.range(0, CONCURRENCY).forEach(i ->
            threads.add(Thread.startVirtualThread(() -> cartService.update(UUID.randomUUID().toString(), 2))));

        threads.forEach(th -> {
            try {
                th.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

In the above test, we’re using the benchmark parameter for concurrency control, and iterating the method 3 times after the initial warm-up.

We’ll now execute the above benchmark test in both JDK 21 and 25 versions.

First, we’ll see the performance report for JDK 21 version:

Benchmark                         (CONCURRENCY)   Mode  Cnt   Score    Error  Units
BenchmarkVirtualThread.benchmark            100  thrpt    6   2.081 ±  0.008  ops/s
BenchmarkVirtualThread.benchmark           1000  thrpt    6   0.214 ±  0.058  ops/s
BenchmarkVirtualThread.benchmark          10000  thrpt    6   0.023 ±  0.001  ops/s
BenchmarkVirtualThread.benchmark            100   avgt    6   0.479 ±  0.011   s/op
BenchmarkVirtualThread.benchmark           1000   avgt    6   4.468 ±  0.033   s/op
BenchmarkVirtualThread.benchmark          10000   avgt    6  44.056 ±  0.279   s/op

Then, let’s confirm the report for JDK 25 version:

Benchmark                         (CONCURRENCY)   Mode  Cnt   Score   Error  Units
BenchmarkVirtualThread.benchmark            100  thrpt    6  18.392 ± 0.206  ops/s
BenchmarkVirtualThread.benchmark           1000  thrpt    6  10.061 ± 0.170  ops/s
BenchmarkVirtualThread.benchmark          10000  thrpt    6   1.005 ± 0.029  ops/s
BenchmarkVirtualThread.benchmark            100   avgt    6   0.058 ± 0.015   s/op
BenchmarkVirtualThread.benchmark           1000   avgt    6   0.108 ± 0.036   s/op
BenchmarkVirtualThread.benchmark          10000   avgt    6   0.962 ± 0.030   s/op

From the above data, we can confirm that the throughput and average time have significantly improved in the latest JDK version.

6. Conclusion

In this article, we’ve learned. We’ve implemented a fix for the synchronized method scenario. Also, we’ve explored how, in the later version, some of the pinning issues are resolved.

As always, the example code can be found over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)