Why Concurrency Is Hard: The Surprising Result of Two Threads Incrementing an int
The article explains why Java concurrency is difficult by showing that two threads each incrementing an int ten thousand times may not produce twenty thousand, then explores the motivations for multithreading, the differences between concurrency and parallelism, and the three core challenges of visibility, atomicity, and ordering.
1. Why We Need Concurrency
Modern CPUs have stopped increasing clock speed and now rely on many cores. A single‑threaded program can only use one core, leaving the others idle. To fully utilize hardware we must split work across multiple threads so that CPU time is not wasted while waiting for I/O, database queries, network calls, or disk access.
Concurrency (the programming model) is not the same as parallelism (the runtime effect). On a single core, the OS rapidly switches between threads to give the illusion of simultaneous execution; on multiple cores, true parallelism can be achieved if the scheduler places threads on different cores.
2. Processes, Threads and Context Switching
A process is the operating‑system unit of resource allocation, providing an isolated memory address space. A thread is the unit of CPU scheduling; all threads within a process share that process's memory, heap, static variables, and open file handles.
Because threads share memory, data races, visibility problems, and atomicity issues arise. Context switching saves the current thread’s registers and program counter, restores the next thread’s state, and, if the threads belong to different processes, switches the memory address space, causing TLB and cache invalidation. A single switch typically costs a few microseconds, but frequent switches can create a "context‑switch storm" and hidden cache‑miss penalties, explaining why adding more threads than CPU cores can degrade performance.
On Linux, vmstat 1 shows the per‑second context‑switch count in the cs column; pidstat -w -p <pid> 1 distinguishes voluntary ( cswch/s ) from involuntary ( nvcswch/s ) switches.
3. The Three Ghosts: Visibility, Atomicity, Ordering
These three problems explain why the simple code "two threads each increment an int ten thousand times" may not yield twenty thousand.
Atomicity : A single statement like count++ compiles to three separate steps—read, modify, write. If two threads interleave these steps, one increment can be lost, leading to the classic "lost update" bug (e.g., overselling inventory).
Visibility : Each CPU core has its own cache. Without proper synchronization, a write performed by one thread may stay in that core’s cache and not become visible to another thread, causing loops that never observe a changed flag. The volatile keyword forces a write to main memory and a read to fetch the latest value.
Ordering : Compilers and CPUs may reorder instructions as long as single‑threaded semantics are preserved. In multithreaded code this can expose intermediate states, such as the double‑checked locking pattern creating a partially constructed singleton.
When evaluating any concurrency tool, ask which of these ghosts it addresses. For example, synchronized, Lock, and CAS target atomicity; volatile and synchronized also address visibility and ordering; the Java Memory Model (JMM) and happens‑before rules define the underlying guarantees.
4. Thread Lifecycle: Six States
Java defines six states in java.lang.Thread.State: NEW: Thread object created but start() not called. RUNNABLE: Either actually executing on the CPU or ready to be scheduled. BLOCKED: Waiting to acquire a monitor lock (e.g., synchronized). WAITING: Waiting indefinitely for another thread’s action (e.g., Object.wait()). TIMED_WAITING: Waiting with a timeout (e.g., Thread.sleep(), Object.wait(long), LockSupport.parkNanos()). TERMINATED: run() completed or the thread terminated by an exception.
Common pitfalls:
Java merges the OS "ready" and "running" states into a single RUNNABLE state, so a thread shown as RUNNABLE may actually be waiting for CPU. BLOCKED only appears when a thread is blocked on a monitor lock; waiting on a ReentrantLock or LockSupport.park() shows as WAITING or TIMED_WAITING instead. WAITING and TIMED_WAITING differ only by the presence of a timeout parameter.
Thread creation in practice is essentially new Thread() followed by start(). Other approaches (subclassing Thread, implementing Runnable or Callable, using ExecutorService) are merely ways to supply tasks to a thread or thread pool.
Daemon threads (created via thread.setDaemon(true) before start()) differ from user threads only in JVM shutdown semantics: the JVM exits when all user threads finish, regardless of daemon threads. The garbage‑collector thread is a typical daemon.
5. Interruption: Stopping a Thread Cooperatively
The deprecated Thread.stop() is unsafe because it can terminate a thread at an arbitrary point, leaving shared data in an inconsistent state. Java instead uses cooperative interruption: a thread checks its interrupt flag and decides when to exit.
Key methods:
thread.interrupt(); // set interrupt flag to true
thread.isInterrupted(); // read flag without clearing
Thread.interrupted(); // read flag and clear it (static, affects current thread)A typical pattern:
public void run() {
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
cleanup();
}When a thread is blocked in sleep(), wait(), or join(), an interrupt causes those methods to throw InterruptedException and clear the flag. The correct handling is either to re‑throw the exception or restore the interrupt flag:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag
// decide whether to exit or continue
}This cooperative model recurs throughout the series: wait/notify, Condition, thread‑pool shutdown, etc., all rely on the same principle of signaling rather than forcing.
6. Full Series Map
The article concludes with a roadmap for the remaining 13 parts, each tackling how to tame the three ghosts:
Part 02 – Java Memory Model (JMM) – formal rules behind visibility, ordering, and happens‑before.
Parts 03‑06 – Basic locks ( volatile, synchronized), CAS, AQS, ReentrantLock.
Part 07 – Deadlocks, thread‑safety strategies, immutable objects.
Part 08 – Thread pools, sizing, and tuning.
Parts 09‑10 – Concurrent containers, blocking queues, producer‑consumer.
Parts 11‑12 – Coordination tools ( CountDownLatch, CyclicBarrier, Semaphore, ThreadLocal).
Parts 13‑14 – Asynchronous programming with CompletableFuture, Fork/Join, and finally virtual threads (JDK 21) that may rewrite the whole concurrency model.
All examples target JDK 8, with version notes for later features such as biased locking (disabled by default since JDK 15) and virtual threads (standardized in JDK 21).
Understanding the underlying hardware mechanisms—multiple cores, caches, compiler reordering—turns concurrency from mysticism into reasoned engineering.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
