Concurrency & threading
SeniorPlatform threads vs virtual threads and why Project Loom changes the cost model, plus the concurrency contracts — synchronized, volatile, double-checked locking, and common races.
Created 16 September 2026 · Updated 23 October 2026
3 concepts · detail hidden
01Glossary — terms used in this page
#c-glossary-terms-used-in-this-page- Platform thread — Java’s traditional
Thread, a thin wrapper around one OS thread. - Virtual thread — a lightweight, JVM-scheduled thread multiplexed onto a small pool of platform threads.
- Carrier thread — the platform thread a virtual thread is currently mounted on and running.
- Project Loom / JEP (JDK Enhancement Proposal) — the OpenJDK project that delivered virtual threads; a JEP is a numbered proposal for a JDK change (virtual threads are JEP 444).
- Monitor — the lock every Java object carries, acquired and released by
synchronized. - JMM (Java Memory Model) — the specification of when one thread’s writes become visible to another.
- Happens-before — the JMM ordering guarantee that makes a write visible to a later read.
- Safe publication — making an object visible to other threads in a fully constructed state.
volatile— field modifier that makes every read see the latest write and forbids reordering around it; it does not make compound operations atomic.- Spurious wakeup — a thread returning from
wait()without having been notified.
- Platform thread — Java’s traditional
02Virtual threads — what they are, and why they're superior to platform threads for I/O-bound work
#c-virtual-threads-what-they-are-and-why-theyre-superior-to-plaVirtual threads (a stable JDK feature since Java 21, delivered by Project Loom, JEP 444) are lightweight threads scheduled by the JVM itself, rather than mapped 1:1 onto a dedicated OS thread. A platform thread — Java’s traditional
Thread— is a thin wrapper around a genuine OS thread: expensive to create (typically a megabyte-scale stack reserved per thread by default) and practically limited to at most a few thousand concurrently before OS scheduling overhead and memory pressure dominate. A virtual thread instead multiplexes huge numbers of these JVM-managed threads — potentially millions — onto a much smaller pool of underlying platform (“carrier”) threads: when a virtual thread performs a blocking operation on a Loom-aware API (blocking I/O,Thread.sleep, thejava.util.concurrentutilities), the JVM automatically unmounts it from its carrier thread, freeing that OS thread to run a different virtual thread, and later re-mounts it — possibly on a different carrier thread — once the blocking call completes. From the application code’s own perspective this is completely transparent: you write the exact same simple, sequential, blocking-style code you’d always write (socket.read(), a blocking JDBC call), with none of the callback/reactor-style inversion of control that reactive frameworks (Netty, reactive streams) traditionally require to reach comparable scalability. This is precisely why virtual threads are described as superior specifically for I/O-bound, high-concurrency workloads — a typical web server handling many concurrent, mostly-waiting requests gets thread-per-request-style code that genuinely scales to huge numbers of concurrently blocked requests, without either the OS-thread ceiling of platform threads or the code complexity of async/reactive programming. They deliberately don’t help CPU-bound work at all: a virtual thread actually running CPU-bound code still occupies a real carrier thread for the entire duration, so CPU-bound throughput stays bounded by the number of actual cores exactly as with platform threads — the win is specifically about how little OS-thread resource is consumed while waiting, not about doing more computation in parallel.Conceptually, this is the JVM’s own version of what
asyncio’s cooperative scheduling gives Python — many logical tasks multiplexed onto a small number of real OS threads — but implemented transparently by the JVM scheduler itself, rather than requiringasync/awaitand a differently-“colored” function signature for every call that might block.03Concurrency contracts — visibility, synchronized, and double-checked locking
#c-concurrency-contracts-visibility-synchronized-and-double-chesynchronized needs the same lock
class Counter { private int n; synchronized void inc() { n++; } // locks `this` void bad() { synchronized (new Object()) { n++; } } // different lock every time: no exclusion }A lock only protects data if every access (reads and writes) uses the same monitor.
synchronizedalso establishes happens-before: unlock then lock on the same monitor makes writes visible.Singleton with double-checked locking
class Config { private static volatile Config instance; // volatile is REQUIRED static Config get() { Config local = instance; if (local == null) { synchronized (Config.class) { local = instance; if (local == null) instance = local = new Config(); } } return local; } }Without
volatile, the JIT/CPU may reorder “write the reference” before “run the constructor”, so another thread sees a non-nullinstancepointing at a half-initialised object — a failure of safe publication. Thevolatilewrite/read pair restores happens-before between the constructor finishing and another thread reading the reference. Simpler and safe alternatives: the initialization-on-demand holder idiom, or anenumsingleton.class Config { private Config() {} private static class Holder { static final Config INSTANCE = new Config(); } static Config get() { return Holder.INSTANCE; } // class init is thread-safe, lazy }Other traps
i++on avolatile intis not atomic (read, add, write); useAtomicInteger.- Check-then-act on a
ConcurrentHashMap(if (!map.containsKey(k)) map.put(k, v)) races; useputIfAbsentorcomputeIfAbsent. longanddoublewrites on 32-bit JVMs withoutvolatilecould tear (rare today, still spec-legal).wait()must be called in a loop that rechecks its condition (spurious wakeups) and while holding the monitor.Thread.run()runs on the current thread; onlystart()creates a new one.SimpleDateFormatand other non-thread-safe classes shared across threads; preferjava.time.