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

  1. Avoid synchronized with Blocking Operations

    // Bad
    synchronized (lock) {
        blockingIO();
    }
    
    // Good
    reentrantLock.lock();
    try {
        blockingIO();
    } finally {
        reentrantLock.unlock();
    }
    
  2. Use Concurrent Collections

    // Prefer
    ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue
    
    // Over
    synchronized + HashMap/ArrayList/LinkedList
    
  3. Keep Critical Sections Short

    // Do blocking outside lock
    Data data = fetchData();  // Outside lock
    
    lock.lock();
    try {
        updateState(data);  // Quick update
    } finally {
        lock.unlock();
    }
    
  4. Monitor for Pinning

    -Djdk.tracePinnedThreads=full
    -XX:StartFlightRecording
    
  5. Use Lock-Free Algorithms When Possible

    AtomicInteger, AtomicReference, CAS operations