Java Memory Model and Thread SafetyIntroductionJava allows several threads to execute concurrently within one application. Concurrency can improve responsiveness and throughput, but it also introduces a class of errors that may not appear in sequential testing. Two threads may update the same object, one thread may observe an incomplete state, or operations may occur in an order different from the order suggested by the source code. The Java Memory Model (JMM) defines the rules governing how threads interact through shared memory. It explains when a write performed by one thread becomes visible to another thread, which operations are ordered, and which compound actions are indivisible. Thread safety is the practical goal: a class or program is thread-safe when its behavior remains correct under permitted concurrent execution. The JMM does not require every thread to observe every operation immediately. Instead, it defines guarantees created by synchronization, volatile access, thread lifecycle operations, and other specified relationships. Correct concurrent programming therefore requires more than identifying shared variables; it requires establishing appropriate visibility, atomicity, and ordering guarantees. Learning ObjectivesAfter studying this chapter, you should be able to: - distinguish atomicity, visibility, and ordering;
- explain data races and race conditions;
- interpret happens-before relationships;
- use synchronized methods and blocks correctly;
- apply volatile fields when the required semantics are suitable;
- use atomic variables for lock-free compound updates;
- design immutable classes and safely publish objects;
- identify unsafe concurrency patterns and improve them;
- evaluate thread-safety decisions in a realistic Java application.
1. Shared Memory and the Java Memory ModelA Java thread has its own execution state, including a stack and program counter, but threads in the same process can access objects stored in the shared heap. A simplified model is: - Thread-local state: local variables and method-call data;
- Shared state: instance fields, static fields, and array elements accessible by multiple threads;
- Synchronization mechanisms: locks, volatile fields, atomic classes, thread start and termination rules, and concurrent collections.
The JMM is an abstract specification rather than a direct description of a particular processor cache. Compilers, the Java Virtual Machine, and hardware may reorder operations when doing so cannot change the behavior of a correctly synchronized program. Consequently, source-code order alone is not sufficient evidence of cross-thread ordering. For example: class Counter {
private int value;
public void increment() {
value++;
}
public int get() {
return value;
}
}If two threads call increment(), the expression value++ is not one indivisible action. It consists conceptually of reading value, adding one, and writing the result. Both threads can read the same old value and then write the same new value. This is a lost update. 2. Atomicity, Visibility, and OrderingThese three properties are related but distinct. 2.1 AtomicityAn operation is atomic if it occurs as one indivisible action from the perspective of other threads. No thread can observe a permitted intermediate state of that operation. Reads and writes of most primitive variables and references are individually atomic. However, a compound expression such as count++, if (!closed) close(), or balance = balance - amount is not automatically atomic. Atomic individual steps do not make a sequence atomic. A long or double field should be treated through the JMM guarantees applicable to the Java version and declaration. In modern Java, reads and writes of these variables are atomic, but atomicity still does not make compound operations safe. 2.2 VisibilityVisibility means that a read by one thread observes a write performed by another thread, rather than indefinitely observing an old value. Without a happens-before relationship, the JMM does not require a thread to observe another thread’s ordinary write promptly—or in some cases at all. This program is not reliably correct: class Worker {
private boolean shutdown;
public void requestShutdown() {
shutdown = true;
}
public void run() {
while (!shutdown) {
doWork();
}
}
}The worker may continue looping because the ordinary write to shutdown has no specified cross-thread visibility relationship with the read. Declaring the field volatile, or protecting both accesses with the same lock, provides the required visibility. 2.3 OrderingOrdering concerns the permitted sequence in which operations become observable. A compiler or processor can reorder independent operations if the resulting behavior remains valid under the JMM. Synchronization prevents certain reorderings and establishes an order visible to other threads. Ordering matters during publication. Consider: class Config {
int port;
boolean secure;
}
class Service {
private Config config;
void initialize() {
Config c = new Config();
c.port = 8443;
c.secure = true;
config = c;
}
}If config is exposed to another thread without safe publication, that thread is not guaranteed to observe the fully initialized state. The reference assignment and field initialization need an appropriate happens-before mechanism. 3. Data Races and Race ConditionsA data race occurs when two threads access the same variable concurrently, at least one access is a write, and the accesses are not ordered by a happens-before relationship. A data race is a property of memory accesses. A race condition is a broader correctness problem in which the result depends improperly on timing or interleaving. Race conditions can occur even when individual accesses are atomic. For example, two threads may both check whether a seat is available and then both reserve it. Each check may be atomic, but the check-and-reserve policy is not. Thread safety requires protecting invariants. An invariant is a condition that must remain true, such as: - an account balance must not become negative;
- a queue’s size must equal the number of stored elements;
- a reservation must be assigned to at most one customer;
- a published object must be completely initialized.
The correct question is not merely “Is this field volatile?” but “What operations must be observed together to preserve the invariant?” 4. Happens-Before RelationshipsThe happens-before relation is a central JMM concept. If action A happens-before action B, then: - the effects of A are visible to B; and
- A is ordered before B for the purposes of the memory model.
Happens-before is not necessarily a wall-clock timestamp. It is a formal ordering relationship that constrains legal executions. Important happens-before rules include the following: | Relationship | Meaning |
|---|
| Program order | Each action in a thread happens-before a later action in that same thread. | | Monitor unlock → subsequent lock | An unlock of a monitor happens-before every later successful lock of that same monitor. | | Volatile write → subsequent volatile read | A write to a volatile field happens-before a later read of that field. | | Thread start | Actions before Thread.start() happen-before actions in the started thread. | | Thread termination | Actions in a thread happen-before another thread successfully returns from join() on it. | | Transitivity | If A happens-before B and B happens-before C, A happens-before C. |
For example: class Job {
private int result;
private boolean complete;
void calculate() {
result = 42;
complete = true;
}
int read() {
if (complete) {
return result;
}
return -1;
}
}If complete is declared volatile, the volatile write of true happens-before a later read that observes true. By program order, the write to result precedes the write to complete; therefore, by transitivity, the result write happens-before the reading thread’s subsequent read of result. The relationship is directional. A volatile read that occurs before the volatile write does not obtain the later write. Also, merely accessing the same ordinary field does not create a happens-before relationship. 5. Synchronization with synchronizedJava monitors provide mutual exclusion and memory visibility. Every object can serve as a monitor. A synchronized method or block acquires a monitor before entering and releases it when leaving, including when an exception causes exit. class SafeCounter {
private int value;
public synchronized void increment() {
value++;
}
public synchronized int get() {
return value;
}
}Both methods use the instance monitor, so only one thread at a time can execute either method for a particular object. The unlock performed by one call happens-before a subsequent lock on the same monitor. Thus synchronization supplies both exclusion and visibility. A block can limit the critical section: class Inventory {
private final Object lock = new Object();
private int units;
public boolean remove(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
synchronized (lock) {
if (units < amount) {
return false;
}
units -= amount;
return true;
}
}
}The validation and update belong in the same critical section because together they preserve the invariant units ≥ 0. Synchronizing only the subtraction would be insufficient. Lock discipline and limitationsA lock is useful only if all accesses that participate in an invariant follow the same locking policy. Common errors include locking on a publicly accessible object, synchronizing reads but not writes, holding a lock during slow external I/O, and acquiring multiple locks in inconsistent order. Nested locks can cause deadlock. If thread A holds lock X and waits for Y while thread B holds Y and waits for X, neither can proceed. Consistent lock ordering, shorter critical sections, and higher-level concurrency utilities reduce this risk. 6. Volatile FieldsA volatile field provides visibility and ordering for accesses to that field. A volatile write happens-before a subsequent volatile read of the same field. Volatile access does not provide mutual exclusion and does not make a compound action atomic. A suitable use is a state flag: class Server implements Runnable {
private volatile boolean running = true;
public void stopServer() {
running = false;
}
@Override
public void run() {
while (running) {
acceptRequest();
}
}
}The flag is suitable because each operation is an independent read or write, and no invariant requires a read-modify-write sequence. This is not fixed by volatile: private volatile int requests;
void record() {
requests++; // still read, add, write
}Use synchronization or an atomic counter instead. A volatile reference guarantees visibility of the reference value, not deep immutability of the object it points to. If a volatile field refers to a mutable object, changes to the object’s fields require their own synchronization strategy. 7. Atomic Variables and Compare-and-SetThe java.util.concurrent.atomic package provides classes such as AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference. They support atomic operations without requiring an application-level lock for each operation. import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private final AtomicInteger value = new AtomicInteger();
public void increment() {
value.incrementAndGet();
}
public int get() {
return value.get();
}
}Many atomic operations use compare-and-set (CAS) conceptually: - read the current value;
- compare it with an expected value;
- replace it with a new value only if the comparison succeeds;
- retry if another thread changed it first.
An atomic variable is appropriate when the state transition can be expressed as an atomic operation or a carefully designed CAS loop. It does not automatically make several independent variables consistent. If an account requires both balance and transaction history to change together, a lock or another transaction-like design may be necessary. Atomic operations may suffer from contention: many threads repeatedly retrying CAS can waste CPU time. For high-throughput counters, LongAdder can provide better scalability when exact instantaneous reads are not required. 8. Immutability and Safe PublicationAn immutable object cannot change observable state after construction. Immutability simplifies concurrency because threads can safely share a correctly constructed immutable object without coordinating later mutations. A robust immutable class generally has: - private final fields;
- no mutator methods;
- complete initialization in the constructor;
- validation of constructor arguments;
- defensive copies for mutable inputs and outputs;
- no escape of
this during construction.
Example: import java.util.List;
public final class UserProfile {
private final String username;
private final List<String> roles;
public UserProfile(String username, List<String> roles) {
this.username = java.util.Objects.requireNonNull(username);
this.roles = List.copyOf(roles);
}
public String username() {
return username;
}
public List<String> roles() {
return roles;
}
}String is immutable, and List.copyOf prevents later changes to the caller’s list from changing the profile. The class is also final, preventing subclasses from adding unsafe mutable behavior.
Final fields have special JMM initialization guarantees when the constructor completes normally and the object is not exposed prematurely. Nevertheless, the object must still be safely published, especially when mutable referenced objects are involved. Safe publication mechanismsAn object can be safely published by: - storing it in a properly initialized
final field; - assigning it to a volatile field;
- publishing it while holding a lock and reading it under the same lock;
- placing it in a thread-safe collection;
- initializing it before starting a thread;
- returning from a task and obtaining its result through the appropriate concurrency mechanism.
The initialization-before-start rule is particularly useful: Config config = loadConfig();
Thread worker = new Thread(() -> use(config));
worker.start(); Actions before start() happen-before actions in the new thread. In contrast, constructing an object and placing it in an ordinary shared field without synchronization is unsafe publication. 9. Thread-Safe Collections and Compound ActionsCollections such as ArrayList and HashMap are not generally safe for concurrent structural modification. Options include synchronized wrappers, concurrent collections, or explicit locking. Map<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("java", 1, Integer::sum);The merge operation is useful because the update of one key is designed as a concurrent map operation. This is safer than separately calling get, adding one, and calling put, which creates a check-and-update race. A concurrent collection does not automatically make a larger workflow atomic. For example, “if key is absent, then insert and notify another subsystem” may require a stronger coordination mechanism than a single map operation. 10. Case Study: Duplicate Reservation in an Online WorkshopBackgroundAn educational platform offers one final place in a workshop. Requests arrive concurrently through a web application. The initial implementation stores the number of available places in an ordinary integer. ProblemThe code is: class Workshop {
private int places = 1;
boolean reserve() {
if (places > 0) {
places--;
return true;
}
return false;
}
}Two request threads can both read places == 1, both pass the condition, and both decrement. The platform may confirm two reservations for one place. This is a race condition and violates the business invariant that successful reservations must not exceed capacity. AnalysisThe condition test and decrement form one logical transaction. Making places volatile would improve visibility but would not combine the test and decrement atomically. An atomic integer can express the transition, but the result must distinguish successful and unsuccessful reservation. Solution using synchronizationclass SynchronizedWorkshop {
private int places = 1;
public synchronized boolean reserve() {
if (places == 0) {
return false;
}
places--;
return true;
}
}The monitor makes the check and update mutually exclusive and makes prior updates visible. Alternative using an atomic variableimport java.util.concurrent.atomic.AtomicInteger;
class AtomicWorkshop {
private final AtomicInteger places = new AtomicInteger(1);
public boolean reserve() {
while (true) {
int current = places.get();
if (current == 0) {
return false;
}
if (places.compareAndSet(current, current - 1)) {
return true;
}
}
}
}If another thread changes the value after get, CAS fails and the loop retries. Exactly one thread can change one remaining place from 1 to 0. Outcome and learning pointsThe synchronized design is simpler and often preferable when the invariant involves several fields or when contention is moderate. The atomic design can avoid blocking for this single-variable state transition but is more complex and may retry under contention. The key lesson is that the synchronization mechanism must match the invariant, not merely the data type. 11. Common Errors and Best PracticesFrequent errors- Assuming source-code order is automatically cross-thread order.
- Declaring a counter volatile and expecting
++ to be safe. - Publishing
this from a constructor. - Returning internal mutable collections directly.
- Synchronizing on a newly created object in each method, which provides no shared exclusion.
- Mixing synchronized and unsynchronized access to the same invariant.
- Using a concurrent collection while assuming an entire multi-step workflow is atomic.
- Holding locks during network calls or unbounded waits.
- Ignoring interruption and cancellation in worker threads.
- Testing only under light load and assuming absence of failure proves correctness.
Professional practices- Define the shared-state invariant before selecting a mechanism.
- Prefer immutable data for configuration and messages.
- Minimize shared mutable state.
- Document the lock protecting each field or invariant.
- Use
final wherever a reference should not change. - Choose
volatile for independent state signals, not compound updates. - Prefer high-level utilities such as
ExecutorService, concurrent collections, and atomic classes where appropriate. - Stress-test with repeated schedules and realistic contention.
- Treat thread safety as part of the API contract.
- Review cancellation, failure, shutdown, and visibility behavior together.
12. Limitations and Contemporary ConsiderationsThe JMM enables portable concurrency, but it does not eliminate design complexity. Correctness proofs become difficult when many locks, callbacks, mutable objects, and external systems interact. Performance optimizations can also create trade-offs: a larger critical section may be easier to prove correct but reduce throughput; lock-free code may reduce blocking but increase algorithmic complexity. Modern Java applications commonly combine the JMM with executors, futures, structured task management, concurrent collections, and immutable data transfer objects. These abstractions do not replace memory-model reasoning. A task result, queue insertion, or future completion is useful precisely because its specification establishes appropriate visibility and ordering guarantees. Security and governance also matter. A visibility bug in authentication, authorization, financial limits, or audit logging can become a serious integrity or confidentiality incident. Shared security state should have an explicit publication and update policy, and concurrent tests should be part of code review and release governance. Chapter SummaryThe Java Memory Model defines how Java threads observe and order actions on shared data. Atomicity concerns indivisible operations; visibility concerns whether one thread observes another’s writes; ordering concerns the sequence in which actions are constrained. Data races arise when conflicting accesses lack a happens-before relationship, while race conditions are broader timing-dependent correctness failures. Happens-before relationships are created by program order, monitor unlock and lock, volatile write and read, thread start, thread termination and join, and transitivity. synchronized provides mutual exclusion and visibility for code using the same monitor. volatile provides visibility and ordering for a field but not compound-operation atomicity. Atomic classes support operations such as increment and compare-and-set, while immutability and safe publication reduce the need for coordination. Correct design begins with identifying invariants and selecting a mechanism that protects the complete logical operation. Key TermsJava Memory Model: Rules defining the permitted visibility and ordering of actions between Java threads. Atomicity: The property that an operation is indivisible to other threads. Visibility: The property that one thread can observe another thread’s write. Ordering: Constraints on the order in which actions are observed. Happens-before: A JMM relationship that guarantees visibility and ordering from one action to another. Data race: Conflicting concurrent accesses without a happens-before relationship. Race condition: A correctness failure dependent on timing or interleaving. Invariant: A condition that must remain true during program execution. Monitor: The lock and associated synchronization mechanism of a Java object. Critical section: Code that accesses shared state and must be coordinated. Volatile: A field modifier providing visibility and ordering for that field. Atomic variable: A concurrency class supporting indivisible state operations. Compare-and-set: An operation that updates a value only if it equals an expected value. Safe publication: Making an object visible so that its initialized state is reliably observed. Immutability: A design property in which observable object state cannot change after construction. Defensive copy: A copy that prevents external mutation of an internal mutable object. Lock contention: Competition among threads attempting to acquire the same lock. Deadlock: A state in which threads wait indefinitely for one another. Thread safety: Correct behavior under permitted concurrent execution. Review QuestionsShort Answer Questions- Define atomicity, visibility, and ordering.
- What is a happens-before relationship?
- Why does
volatile not make counter++ thread-safe? - State two mechanisms for safe publication.
- What invariant does the workshop reservation example protect?
Long Answer Questions- Explain how
synchronized provides both mutual exclusion and memory visibility. - Compare ordinary fields, volatile fields, and atomic variables.
- Describe the main happens-before rules and illustrate transitivity.
- Explain how immutability and defensive copying support thread safety.
- Discuss common causes of deadlock and methods for reducing its risk.
Analytical / Application Questions- A worker reads a non-volatile
stop flag in a loop while another thread writes it. Analyze the possible problem and propose two correct designs. - A banking operation checks a balance, subtracts an amount, and records a transaction in a list. Decide whether volatile, an atomic integer, or a lock-based design is most suitable, and justify your answer.
- A service publishes a mutable configuration object through an ordinary static field. Identify the publication risks and redesign the publication mechanism.
Multiple Choice Questions with AnswersQuestion 1Which property means that a thread observes a write made by another thread? A. Atomicity B. Visibility C. Inheritance D. Encapsulation Correct Answer: B. Visibility Question 2Why is value++ unsafe when multiple threads update an ordinary shared integer? A. It always causes a syntax error B. It performs a read, modification, and write that are not one atomic action C. Integers cannot be shared between threads D. The JVM converts every integer to a string Correct Answer: B. It performs a read, modification, and write that are not one atomic action Question 3Which relationship is established by an unlock of a monitor followed by a later lock of the same monitor? A. The unlock happens-before the later lock B. The later lock happens-before the unlock C. They are unrelated D. They create a new thread Correct Answer: A. The unlock happens-before the later lock Question 4Which is an appropriate use of a volatile field? A. A counter updated with count++ B. A state flag independently written and read by threads C. A multi-field bank transfer D. A check-then-insert operation Correct Answer: B. A state flag independently written and read by threads Question 5What does a successful compare-and-set operation do? A. Updates a value regardless of its current content B. Updates a value only when it equals the expected value C. Acquires every lock in the program D. Stops all other threads permanently Correct Answer: B. Updates a value only when it equals the expected value Question 6Which feature most directly prevents external modification of a stored mutable list in an immutable class? A. Returning the original list B. Using a defensive copy or an unmodifiable copy C. Declaring the list local in a method D. Making the list public Correct Answer: B. Using a defensive copy or an unmodifiable copy Question 7What is the principal problem in the workshop reservation example? A. The capacity check and decrement are not one protected operation B. The integer is too large for Java C. The method cannot return a boolean D. The class must extend Thread Correct Answer: A. The capacity check and decrement are not one protected operation Question 8Which operation creates a happens-before relationship from the parent to the new thread? A. Calling Thread.start() after the parent’s prior actions B. Calling Thread.sleep() without communication C. Reading an unrelated ordinary field D. Constructing two unrelated objects Correct Answer: A. Calling Thread.start() after the parent’s prior actions Question 9Which statement about synchronized access is correct? A. Only writes need to use the monitor B. All accesses participating in the invariant should follow the same locking policy C. A different lock object in every method provides mutual exclusion D. Synchronization removes the possibility of all programming errors Correct Answer: B. All accesses participating in the invariant should follow the same locking policy Question 10A thread performs actions and another thread successfully returns from join() on it. What is guaranteed? A. The joined thread automatically becomes immortal B. The first thread’s prior actions happen-before the returning thread continues after join() C. All fields become volatile permanently D. No other thread can execute Correct Answer: B. The first thread’s prior actions happen-before the returning thread continues after join()
|