8.2 Pinning and Synchronization
Understanding thread pinning is critical for effective virtual thread usage. Pinning occurs when a virtual thread cannot be unmounted from its carrier thread, limiting scalability.
What is Pinning?
Normal Virtual Thread Behavior:
Virtual Thread blocks (I/O, sleep)
↓
Unmounts from Carrier Thread
↓
Carrier Thread picks up another Virtual Thread
↓
When I/O completes, Virtual Thread remounts on any available Carrier
Pinned Virtual Thread:
Virtual Thread blocks inside synchronized block
↓
CANNOT unmount - stays pinned to Carrier Thread
↓
Carrier Thread is blocked (wasted)
↓
Other Virtual Threads cannot use that Carrier
Causes of Pinning
1. Synchronized Blocks/Methods
// BAD: Pinning occurs
public class PinningExample {
private final Object lock = new Object();
public void processRequest() {
synchronized (lock) { // Virtual thread pins carrier
// If this blocks, carrier thread is wasted
String data = fetchFromDatabase(); // Blocking I/O
processData(data);
}
}
}
2. Native Methods
// Native methods can pin virtual threads
public native void nativeOperation(); // May cause pinning
public void process() {
nativeOperation(); // Virtual thread might pin
}
Detecting Pinning
JVM Options:
# Enable pinning detection
java -Djdk.tracePinnedThreads=full MyApp
# Output when pinning detected:
# Thread[#23,ForkJoinPool-1-worker-1,5,CarrierThreads]
# java.base/java.lang.VirtualThread$VThreadContinuation.onPinned
# java.base/java.lang.VirtualThread.parkNanos
# ...
# app//MyClass.synchronizedMethod(MyClass.java:42)
Programmatic Detection:
import jdk.internal.vm.Continuation;
public class PinningDetector {
public static boolean isPinned() {
// Internal API - not recommended for production
// Use JFR events instead
return Continuation.isPinned(Continuation.getCurrentContinuation());
}
}
Java Flight Recorder (JFR):
# Start recording
java -XX:StartFlightRecording=filename=recording.jfr MyApp
# Analyze with JFR
jfr print --events jdk.VirtualThreadPinned recording.jfr
Avoiding Pinning with synchronized
Problem: Synchronized Block
public class UserService {
private final Map<String, User> cache = new HashMap<>();
// BAD: synchronized can pin
public synchronized User getUser(String id) {
User cached = cache.get(id);
if (cached != null) {
return cached;
}
// Blocking database call - virtual thread pinned!
User user = database.fetchUser(id);
cache.put(id, user);
return user;
}
}
Solution 1: Use ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class UserService {
private final Map<String, User> cache = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
// GOOD: ReentrantLock doesn't pin
public User getUser(String id) {
lock.lock();
try {
User cached = cache.get(id);
if (cached != null) {
return cached;
}
// Blocking call - virtual thread can unmount
User user = database.fetchUser(id);
cache.put(id, user);
return user;
} finally {
lock.unlock();
}
}
}
Solution 2: Use Concurrent Collections
import java.util.concurrent.ConcurrentHashMap;
public class UserService {
// BEST: No locks needed
private final Map<String, User> cache = new ConcurrentHashMap<>();
public User getUser(String id) {
return cache.computeIfAbsent(id, key -> {
// Blocking call - no pinning
return database.fetchUser(key);
});
}
}
Solution 3: Reduce Critical Section
public class UserService {
private final Map<String, User> cache = new HashMap<>();
public User getUser(String id) {
// Check cache without lock
synchronized (cache) {
User cached = cache.get(id);
if (cached != null) {
return cached;
}
}
// Fetch outside synchronized block - no pinning
User user = database.fetchUser(id);
// Update cache with lock
synchronized (cache) {
cache.put(id, user);
}
return user;
}
}
Synchronized vs ReentrantLock
Comparison:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Pinning | Yes | No |
| Try-lock | No | Yes (tryLock()) |
| Timed lock | No | Yes (tryLock(timeout)) |
| Interruptible | No | Yes (lockInterruptibly()) |
| Fair/Unfair | Unfair only | Both |
| Condition variables | One | Multiple |
| Syntax | Simple | Verbose |
When to Use Each:
// Use synchronized for:
// 1. Very short critical sections (no blocking)
public synchronized void incrementCounter() {
counter++; // Quick, non-blocking
}
// 2. CPU-bound work only
public synchronized int compute() {
return expensiveCalculation(); // No I/O
}
// Use ReentrantLock for:
// 1. Any blocking operations
private final ReentrantLock lock = new ReentrantLock();
public User fetchUser(String id) {
lock.lock();
try {
return database.query(id); // Blocking I/O
} finally {
lock.unlock();
}
}
// 2. Need advanced features
public boolean tryUpdate(Data data) {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
update(data);
return true;
} finally {
lock.unlock();
}
}
return false;
}
ReentrantLock Patterns
Basic Pattern:
private final ReentrantLock lock = new ReentrantLock();
public void operation() {
lock.lock();
try {
// critical section
modifySharedState();
} finally {
lock.unlock(); // Always in finally!
}
}
Try-Lock Pattern:
public boolean tryOperation() {
if (lock.tryLock()) {
try {
modifySharedState();
return true;
} finally {
lock.unlock();
}
}
return false; // Couldn't acquire lock
}
Timed Lock Pattern:
public void operationWithTimeout() throws TimeoutException {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
modifySharedState();
} finally {
lock.unlock();
}
} else {
throw new TimeoutException("Couldn't acquire lock in 5 seconds");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for lock", e);
}
}
Interruptible Lock Pattern:
public void interruptibleOperation() throws InterruptedException {
lock.lockInterruptibly(); // Can be interrupted while waiting
try {
longRunningTask();
} finally {
lock.unlock();
}
}
Read-Write Lock Pattern:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DataStore {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private String data = "";
// Multiple readers can read concurrently
public String read() {
rwLock.readLock().lock();
try {
// Simulate read operation
Thread.sleep(100);
return data;
} finally {
rwLock.readLock().unlock();
}
}
// Only one writer, blocks all readers
public void write(String newData) {
rwLock.writeLock().lock();
try {
// Simulate write operation
Thread.sleep(100);
data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
}
Lock-Free Alternatives
Atomic Variables:
import java.util.concurrent.atomic.*;
public class LockFreeCounter {
private final AtomicInteger counter = new AtomicInteger(0);
// No locks needed - atomic operation
public int increment() {
return counter.incrementAndGet();
}
public int get() {
return counter.get();
}
// Complex operation with CAS
public boolean incrementIfLessThan(int max) {
while (true) {
int current = counter.get();
if (current >= max) {
return false;
}
if (counter.compareAndSet(current, current + 1)) {
return true;
}
// CAS failed, retry
}
}
}
Concurrent Collections:
import java.util.concurrent.*;
public class LockFreeCache {
// Thread-safe without explicit locks
private final ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();
public User getOrFetch(String userId) {
return cache.computeIfAbsent(userId, id -> {
// This can block for I/O - no pinning!
return database.fetchUser(id);
});
}
public void update(String userId, User user) {
cache.put(userId, user); // Atomic
}
public User updateIfPresent(String userId, Function<User, User> updater) {
return cache.computeIfPresent(userId, (id, user) -> updater.apply(user));
}
}
Real-World Example: Order Processing System
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.time.*;
public class OrderProcessingSystem {
// Use concurrent collections - no locks
private final ConcurrentHashMap<String, Order> orders = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Inventory> inventory = new ConcurrentHashMap<>();
// Use ReentrantLock for complex operations
private final ConcurrentHashMap<String, ReentrantLock> productLocks = new ConcurrentHashMap<>();
public CompletableFuture<Order> processOrder(OrderRequest request) {
return CompletableFuture.supplyAsync(() -> {
String orderId = generateOrderId();
Order order = new Order(orderId, request, OrderStatus.PENDING);
// Store order - no lock needed
orders.put(orderId, order);
try {
// Reserve inventory with per-product locking
reserveInventory(order);
// Process payment (blocking I/O - virtual thread parks)
processPayment(order);
// Update order status
order = order.withStatus(OrderStatus.CONFIRMED);
orders.put(orderId, order);
// Send confirmation (blocking I/O)
sendConfirmation(order);
return order;
} catch (Exception e) {
// Release inventory on error
releaseInventory(order);
order = order.withStatus(OrderStatus.FAILED);
orders.put(orderId, order);
throw new RuntimeException("Order processing failed", e);
}
}, Executors.newVirtualThreadPerTaskExecutor());
}
private void reserveInventory(Order order) {
for (OrderItem item : order.items()) {
String productId = item.productId();
// Get or create lock for this product
ReentrantLock lock = productLocks.computeIfAbsent(
productId,
k -> new ReentrantLock()
);
// Try to acquire lock with timeout
try {
if (!lock.tryLock(5, TimeUnit.SECONDS)) {
throw new TimeoutException("Couldn't acquire inventory lock");
}
try {
Inventory inv = inventory.get(productId);
if (inv == null || inv.available() < item.quantity()) {
throw new InsufficientInventoryException(productId);
}
// Reserve inventory
inventory.put(productId, inv.reserve(item.quantity()));
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while reserving inventory", e);
} catch (TimeoutException e) {
throw new RuntimeException("Inventory lock timeout", e);
}
}
}
private void releaseInventory(Order order) {
for (OrderItem item : order.items()) {
String productId = item.productId();
ReentrantLock lock = productLocks.get(productId);
if (lock != null) {
lock.lock();
try {
Inventory inv = inventory.get(productId);
if (inv != null) {
inventory.put(productId, inv.release(item.quantity()));
}
} finally {
lock.unlock();
}
}
}
}
private void processPayment(Order order) {
// Simulate blocking payment gateway call
try {
Thread.sleep(200); // Virtual thread parks - no pinning
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Payment interrupted", e);
}
}
private void sendConfirmation(Order order) {
// Simulate blocking email service call
try {
Thread.sleep(100); // Virtual thread parks
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis();
}
// Data classes
record Order(String id, OrderRequest request, OrderStatus status) {
Order withStatus(OrderStatus newStatus) {
return new Order(id, request, newStatus);
}
List<OrderItem> items() {
return request.items();
}
}
record OrderRequest(String userId, List<OrderItem> items) {}
record OrderItem(String productId, int quantity) {}
record Inventory(String productId, int available, int reserved) {
Inventory reserve(int quantity) {
return new Inventory(productId, available - quantity, reserved + quantity);
}
Inventory release(int quantity) {
return new Inventory(productId, available + quantity, reserved - quantity);
}
}
enum OrderStatus { PENDING, CONFIRMED, FAILED }
static class InsufficientInventoryException extends RuntimeException {
InsufficientInventoryException(String productId) {
super("Insufficient inventory for product: " + productId);
}
}
}
Monitoring Pinning in Production
JFR Event Monitoring:
import jdk.jfr.*;
@Name("app.VirtualThreadPinning")
@Label("Virtual Thread Pinning Event")
@Category("Application")
public class PinningEvent extends Event {
@Label("Thread Name")
public String threadName;
@Label("Duration (ms)")
public long durationMs;
@Label("Stack Trace")
public String stackTrace;
}
// Usage
public void monitoredOperation() {
synchronized (lock) {
PinningEvent event = new PinningEvent();
event.begin();
try {
// Potentially blocking operation
blockingCall();
} finally {
event.end();
if (event.shouldCommit()) {
event.threadName = Thread.currentThread().getName();
event.durationMs = event.getDuration().toMillis();
event.stackTrace = getStackTrace();
event.commit();
}
}
}
}
Best Practices Summary
Avoid synchronized with Blocking Operations
// Bad synchronized (lock) { blockingIO(); } // Good reentrantLock.lock(); try { blockingIO(); } finally { reentrantLock.unlock(); }Use Concurrent Collections
// Prefer ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue // Over synchronized + HashMap/ArrayList/LinkedListKeep Critical Sections Short
// Do blocking outside lock Data data = fetchData(); // Outside lock lock.lock(); try { updateState(data); // Quick update } finally { lock.unlock(); }Monitor for Pinning
-Djdk.tracePinnedThreads=full -XX:StartFlightRecordingUse Lock-Free Algorithms When Possible
AtomicInteger, AtomicReference, CAS operations