Core Java

Java Virtual Threads Without Pinning

Virtual Threads, introduced as part of Project Loom, provide a lightweight concurrency model that enables Java applications to handle millions of concurrent tasks efficiently. While Virtual Threads simplify concurrent programming by allowing developers to write familiar blocking-style code, certain synchronization techniques can cause thread pinning, which reduces scalability and performance. In this article, we will explore how Virtual Threads work, understand thread pinning, and learn how to synchronize Virtual Threads correctly without pinning the carrier thread.

1. Overview

Traditional Java applications rely on platform threads provided by the operating system. Since each platform thread consumes significant memory and system resources, applications handling a large number of concurrent tasks often face scalability challenges. Virtual Threads solve this problem by introducing lightweight user-mode threads managed by the JVM rather than the operating system. Thousands or even millions of Virtual Threads can be created with minimal resource consumption. However, developers must be aware of synchronization patterns that may accidentally pin a Virtual Thread to its carrier thread, preventing the JVM from efficiently scheduling other Virtual Threads.

1.1 Introduction to Virtual Threads

A Virtual Thread is a lightweight thread managed by the Java Virtual Machine. Unlike traditional platform threads, Virtual Threads are not permanently associated with operating system threads. When a Virtual Thread blocks on supported operations such as:

  • Network I/O
  • File I/O
  • Sleep operations
  • Waiting on modern concurrency constructs

The JVM can unmount the Virtual Thread from its carrier thread and reuse the carrier thread for other Virtual Threads. This capability dramatically improves application scalability.

1.1.1 What is Thread Pinning?

Thread pinning occurs when a Virtual Thread becomes permanently attached to its carrier thread during a blocking operation. Common causes include:

  • Using synchronized blocks with long-running operations
  • Native method invocations
  • Blocking operations while holding intrinsic locks

When pinning occurs, the carrier thread cannot be released, reducing the scalability benefits of Virtual Threads.

1.1.2 Problematic Example Using synchronized

synchronized(lock) {
    Thread.sleep(5000);
}

1.2 Benefits of Using ReentrantLock with Virtual Threads

  • Avoids carrier thread pinning in common synchronization scenarios.
  • Provides explicit lock acquisition and release control.
  • Supports advanced features such as fairness policies and tryLock().
  • Works efficiently with large numbers of Virtual Threads.
  • Improves application scalability and throughput.

2. Code Example

The following example demonstrates how to synchronize multiple Virtual Threads without causing pinning by using ReentrantLock instead of synchronized blocks.

// VirtualThreadSynchronizationDemo.java

import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class VirtualThreadSynchronizationDemo {

    private static final ReentrantLock lock = new ReentrantLock();
    private static int counter = 0;

    public static void main(String[] args) throws Exception {

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

            for (int i = 1; i <= 10; i++) {

                int taskId = i;

                executor.submit(() -> {

                    lock.lock();

                    try {

                        int currentValue = counter;

                        System.out.println(
                                "Task " + taskId +
                                " updating counter from " +
                                currentValue);

                        Thread.sleep(1000);

                        counter = currentValue + 1;

                        System.out.println(
                                "Task " + taskId +
                                " updated counter to " +
                                counter);

                    } catch (InterruptedException e) {

                        Thread.currentThread().interrupt();

                    } finally {

                        lock.unlock();
                    }
                });
            }
        }

        System.out.println();
        System.out.println("Final Counter Value = " + counter);
    }
}

2.1 Code Explanation

The VirtualThreadSynchronizationDemo class demonstrates how to safely synchronize shared data when using Java Virtual Threads. A ReentrantLock named lock is created to protect access to the shared counter variable, ensuring that only one thread can modify it at a time. Inside the main() method, the Executors.newVirtualThreadPerTaskExecutor() creates an executor that launches each submitted task in its own Virtual Thread. A loop submits ten tasks, and each task captures its unique taskId. When a task starts executing, it acquires the lock using lock.lock(), reads the current counter value, and prints a message showing the value before the update. The task then pauses for one second using Thread.sleep(1000) to simulate a blocking operation. After the delay, it increments the counter and prints the updated value. If the thread is interrupted during sleep, the interruption status is restored using Thread.currentThread().interrupt(). The finally block guarantees that the lock is always released through lock.unlock(), preventing deadlocks and ensuring other Virtual Threads can proceed. The executor is automatically closed by the try-with-resources block, which waits for all submitted tasks to complete before exiting. Finally, the program prints the value of the shared counter, which should be 10 because each of the ten Virtual Threads increments the counter exactly once while synchronization prevents race conditions.

2.2 Code Output

Task 1 updating counter from 0
Task 1 updated counter to 1

Task 2 updating counter from 1
Task 2 updated counter to 2

Task 3 updating counter from 2
Task 3 updated counter to 3

Task 4 updating counter from 3
Task 4 updated counter to 4

Task 5 updating counter from 4
Task 5 updated counter to 5

Task 6 updating counter from 5
Task 6 updated counter to 6

Task 7 updating counter from 6
Task 7 updated counter to 7

Task 8 updating counter from 7
Task 8 updated counter to 8

Task 9 updating counter from 8
Task 9 updated counter to 9

Task 10 updating counter from 9
Task 10 updated counter to 10

Final Counter Value = 10

The output shows that each Virtual Thread acquires the lock one at a time and safely updates the shared counter variable. Task 1 reads the initial value of 0 and updates it to 1, followed by Task 2 updating it from 1 to 2, and so on until Task 10 increments the counter from 9 to 10. Because the ReentrantLock ensures exclusive access to the critical section, no two threads modify the counter simultaneously, preventing race conditions and maintaining data consistency. After all ten Virtual Threads complete their execution, the final output displays Final Counter Value = 10, confirming that each task successfully incremented the shared counter exactly once.

3. Conclusion

Virtual Threads significantly improve the scalability of Java applications by allowing millions of lightweight concurrent tasks to execute efficiently. However, developers should be careful when synchronizing shared resources because certain synchronization techniques can lead to thread pinning. Using synchronized blocks around blocking operations may pin Virtual Threads to carrier threads and reduce concurrency benefits. Modern concurrency constructs such as ReentrantLock provide a safer and more scalable alternative for synchronization in Virtual Thread applications. By combining Virtual Threads with Virtual Thread-friendly synchronization mechanisms, developers can build highly concurrent applications that fully leverage the capabilities introduced by Project Loom in modern Java versions.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button