Skip to content
Java

Java Concurrency Explained: Threads, Locks and the Memory Model

By The EbookWale Team · Updated June 16, 2026 · 4 min read

Java concurrency lets multiple threads run at once — powerful but error-prone. Here's how threads, synchronized, locks, the Java Memory Model, thread pools, and Java 21 virtual threads work, with the pitfalls to avoid.

Java concurrency is running multiple threads at once within a program — and unlike many languages, Java threads run in true parallel across CPU cores. That power makes Java excellent for concurrent workloads, but it also opens the door to race conditions and visibility bugs that only appear under specific timing. Concurrency is a Phase 6 topic from the Java roadmap and a defining senior/staff interview area.

Here’s the model and the pitfalls — without disappearing into the deep end.

Threads: doing several things at once

A thread is an independent path of execution. The modern way to run work on one is via an ExecutorService rather than creating raw Thread objects:

ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());     // run a task on a pooled thread
pool.shutdown();

Thread pools reuse a fixed set of threads, avoiding the cost of creating one per task.

The core problem: race conditions

When multiple threads touch shared mutable data, the result can depend on timing:

class Counter {
    private int count = 0;
    public void increment() { count++; }   // NOT atomic!
}

count++ is actually read-modify-write — three steps. Two threads can read the same value, both increment, and one update is lost. That’s a race condition, and it produces bugs that vanish when you try to debug them.

count = 5 Thread A Thread B count = 6 ✗ read 5 read 5 write 6 write 6
Both read 5, both write 6 — one increment is lost (expected 7).

Fixing it: synchronization

The synchronized keyword lets only one thread at a time run a block on a given object’s lock:

public synchronized void increment() { count++; }   // thread-safe

synchronized does two jobs: mutual exclusion (one thread at a time) and visibility (changes become visible to the next thread). For simple counters, the java.util.concurrent.atomic classes are even better:

private AtomicInteger count = new AtomicInteger();
count.incrementAndGet();          // atomic, lock-free

The Java Memory Model and volatile

Beyond races, there’s visibility: without synchronization, one thread’s write to a field may never be seen by another (each can cache it). The Java Memory Model defines when writes become visible. The volatile keyword guarantees that reads and writes of a field go straight to main memory, so all threads see the latest value:

private volatile boolean running = true;   // visible across threads

volatile gives visibility but not atomicity — use it for flags, not for count++.

🔑 REMEMBER — Concurrency has two distinct hazards: race conditions (interleaved updates corrupt data — fix with synchronized, locks, or atomics) and visibility (one thread doesn't see another's changes — fix with volatile or synchronization). They're different problems with different fixes.

The concurrency toolkit

Modern Java rarely uses raw synchronized for everything. The java.util.concurrent package provides higher-level tools:

  • ExecutorService / thread pools — manage threads for you.
  • Atomic classes — lock-free counters and references.
  • Concurrent collectionsConcurrentHashMap instead of a synchronized HashMap (see the Collections framework).
  • Locks (ReentrantLock) — more flexible than synchronized when you need it.
  • CompletableFuture — composing asynchronous tasks.

Virtual threads (Java 21)

A major recent addition: virtual threads are lightweight, JVM-managed threads you can create by the million. They make high-concurrency code (handling thousands of simultaneous requests) simple, letting you write straightforward blocking-style code without exhausting OS threads. They’re one of the biggest reasons modern Java handles concurrency so well.

Common mistakes

  • Unsynchronized shared mutable state — the root of race conditions.
  • Using volatile for compound actions — it gives visibility, not atomicity; count++ still races.
  • Deadlocks — two threads each holding a lock the other needs. Acquire locks in a consistent order.
  • Over-synchronizing — locking too broadly kills the performance you wanted from threads.
  • Sharing a non-thread-safe HashMap — use ConcurrentHashMap.
⚠️ GOTCHA — count++ looks atomic but isn't — it's read, add, write. Two threads can interleave and lose an update. Use synchronized, a ReentrantLock, or an AtomicInteger; never assume a single statement is thread-safe.

Where this fits

Concurrency is Phase 6 of the Java roadmap, built on understanding the JVM and memory, and it’s a major senior/staff interview area.

Threads, the memory model, the java.util.concurrent toolkit, virtual threads, and the concurrency patterns are worked through in depth in Java for Staff Engineers, with the fundamentals introduced in Java in Three Months.

Two hazards, two fixes — guard shared state and ensure visibility, and Java concurrency becomes manageable.

Frequently asked questions

What is concurrency in Java?

Concurrency in Java is running multiple threads at the same time within a program, so it can do several things at once — handle many requests, or use multiple CPU cores. Unlike Python with its GIL, Java threads can run truly in parallel, which makes Java strong for concurrent workloads but also introduces race conditions and visibility bugs to manage.

What is a race condition in Java?

A race condition happens when two or more threads access shared data at the same time and the result depends on the unpredictable timing of their execution. For example, two threads incrementing the same counter can lose updates. You prevent race conditions with synchronization, locks, or atomic classes.

What does the synchronized keyword do in Java?

synchronized ensures only one thread at a time can execute a block or method on a given object, by acquiring that object's intrinsic lock. It prevents race conditions on shared data and also establishes visibility, so changes made by one thread are seen by the next. It is the simplest way to make code thread-safe.

What are virtual threads in Java?

Virtual threads, introduced in Java 21, are lightweight threads managed by the JVM rather than the operating system, so you can run millions of them cheaply. They make high-concurrency code — like handling many simultaneous requests — far simpler to write, using ordinary blocking-style code without exhausting OS threads.