0Pricing
Java Academy · Lesson

Pitfalls: Pinning and ThreadLocals

Avoid blocking carrier threads.

Pitfalls: Pinning and ThreadLocals is a free Java Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Things That Can Go Wrong

Virtual threads are powerful, but two pitfalls can quietly undermine their scalability: pinning and overuse of ThreadLocals.

This lesson explains both and how to avoid them.

What Is Pinning

Pinning happens when a virtual thread cannot unmount from its carrier while blocked. The carrier OS thread stays stuck, defeating the scalability benefit.

When too many carriers are pinned, throughput collapses.

Cause: synchronized Blocking

The classic cause is blocking inside a synchronized block or method. While holding the monitor and then blocking, the virtual thread stays pinned to its carrier.

Note: on JDK 24+ (JEP 491) this limitation was largely removed, but on Java 21 it is a real concern.

Pinning Example to Avoid

This pattern can pin on Java 21: blocking while holding a monitor. It still runs correctly, but it does not scale.

public class Main {
    static final Object lock = new Object();

    static void risky() {
        synchronized (lock) {
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(Main::risky);
        t.join();
        System.out.println("Done (but this blocked inside synchronized)");
    }
}

The Fix: ReentrantLock

Replace synchronized with a ReentrantLock when the critical section may block. Lock APIs are virtual-thread-friendly and allow unmounting.

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static final ReentrantLock lock = new ReentrantLock();

    static void safe() {
        lock.lock();
        try {
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(Main::safe);
        t.join();
        System.out.println("Done without pinning");
    }
}

Diagnosing Pinning

You can ask the JVM to print a stack trace whenever pinning occurs by launching with:

  • -Djdk.tracePinnedThreads=full for full traces
  • -Djdk.tracePinnedThreads=short for one-liners

This helps you locate the offending synchronized sections.

ThreadLocals Get Expensive

ThreadLocal stores per-thread state. With a few platform threads that is fine. With millions of virtual threads, each carrying its own copy, memory balloons.

Audit your libraries: caching, formatting, and context objects often hide ThreadLocals.

ThreadLocal Still Works

ThreadLocal is not forbidden, just use it sparingly. Each virtual thread gets its own value as expected.

public class Main {
    static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(() -> {
            CONTEXT.set("request-42");
            System.out.println("Context: " + CONTEXT.get());
            CONTEXT.remove();
        });
        t.join();
    }
}

Prefer ScopedValue

Java introduced ScopedValue as a lighter alternative for sharing immutable data with a bounded lifetime. It avoids the per-thread mutable storage cost of ThreadLocal and fits the structured concurrency model.

Where you only need to pass read-only context down a call chain, reach for it instead.

Do Not Pool

Another subtle pitfall is treating virtual threads like a scarce resource and pooling them. That reintroduces contention and ThreadLocal leakage between tasks.

Always create one virtual thread per task and let it terminate.

Checklist for Scale

Before going to production with virtual threads:

  • Replace blocking synchronized with ReentrantLock
  • Enable jdk.tracePinnedThreads in testing
  • Minimize ThreadLocal; prefer ScopedValue
  • Never pool virtual threads

Quick Check

Identify the safe replacement for a blocking synchronized block.

Recap

You learned the main pitfalls:

  • Pinning: blocking inside synchronized ties up a carrier; fix with ReentrantLock
  • Diagnose with -Djdk.tracePinnedThreads
  • ThreadLocal overhead scales with thread count; prefer ScopedValue
  • Never pool virtual threads

That completes the Virtual Threads course.

Frequently asked questions

Is the “Pitfalls: Pinning and ThreadLocals” lesson free?

Yes — the full text of “Pitfalls: Pinning and ThreadLocals” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.

What will I learn in “Pitfalls: Pinning and ThreadLocals”?

Avoid blocking carrier threads. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Java Academy?

No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Pitfalls: Pinning and ThreadLocals” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Java Academy lesson?

Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. What Are Virtual Threads
  2. Creating Virtual Threads
  3. Platform vs Virtual Threads
  4. Pitfalls: Pinning and ThreadLocals
← Back to Java Academy