Why Multithreaded Bugs Happen: Visibility, Atomicity, and Ordering Explained
This article explores how CPU, memory, and I/O speed differences create concurrency bugs, detailing the three core issues of visibility, atomicity, and ordering in Java's memory model and showing practical code examples and solutions.
All design originates from life. In the previous chapter we discussed how to extract maximum CPU, memory, and I/O value, but their speeds differ (CPU > memory > I/O). Balancing these resources is key to improving overall performance.
Three Major Problems
Visibility
A thread’s modification of a shared variable must be immediately visible to other threads; this is called visibility.
The Java Memory Model (JMM) defines that all variables reside in main memory. When a thread accesses a variable, it copies it to its own working memory (private memory) and operates on that copy.
Think of Git: the remote repository is main memory, and the local repository is a thread’s working memory.
Illustration:
Main memory contains variable x = 0. Thread A copies x to its private memory and increments it. Thread A’s write‑back to main memory occurs at an unpredictable time. If Thread B reads x before A writes back, it also sees 0, leading to an incorrect final value.
Thus, visibility problems arise when threads do not promptly refresh main memory.
In Java, all instance fields, static fields, and array elements are stored in heap memory, which is shared among threads; local variables and method parameters are not shared and therefore not subject to visibility issues.
To solve visibility, every thread must read/write the main memory, typically using the volatile keyword.
Atomicity
Atomic operations cannot be interrupted by the thread scheduler; they run from start to finish without context switches.
Example program (counter increment):
Running this under multiple threads does not guarantee count = 20000 because count++ expands to three CPU instructions, losing atomicity. javap -c UnsafeCounter Disassembled snippet shows the four steps: load, increment, store.
To enforce atomicity, you can use synchronized or non‑blocking CAS (Compare‑And‑Swap) utilities like AtomicLong, which rely on the low‑level Unsafe class.
private static final Unsafe unsafe = Unsafe.getUnsafe();Ordering
Compilers may reorder statements for optimization, which can break program logic when the order matters.
Original code:
a = 1;</code><code>b = 2;</code><code>System.out.println(a);</code><code>System.out.println(b);After optimization:
b = 2;</code><code>a = 1;</code><code>System.out.println(a);</code><code>System.out.println(b);Such reordering can cause subtle bugs, e.g., in double‑checked locking for singletons, where the object reference may become visible before the object is fully initialized.
Illustration of the problematic reordering:
Marking the instance variable as volatile or final prevents this issue.
Summary
The program you see is not the raw CPU instructions; the “elephant in the fridge” analogy hides three steps that correspond to visibility, atomicity, and ordering— the root causes of concurrency bugs.
This section described the three fundamental problems; the next articles will analyze solutions for each.
Soul Questions
Why does declaring a variable final make it thread‑safe?
Do you often inspect CPU assembly instructions?
If you were to implement a singleton, which approach would you choose?
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
