Shared state and synchronized
Race conditions on mutable shared state, critical sections, intrinsic locks, and synchronized methods vs blocks.
The race condition
When two threads read and write the same mutable state, the result depends on their interleaving. Consider an unsynchronized counter:
class Counter {
private int count = 0;
public void increment() { count++; } // NOT atomic
public int get() { return count; }
}
count++ looks like one step but is really three: read count, add 1, write back. If two threads each run increment() and interleave their read/write, one update is lost. Run 1,000 increments from two threads on this class and you often get *less* than 2,000 — a race condition on a critical section (the code that touches shared state).
synchronized and the intrinsic lock
Every Java object owns one intrinsic lock (also called its *monitor*). The synchronized keyword acquires that lock on entry and releases it on exit — including when an exception unwinds the block. While one thread holds an object's lock, no other thread can enter any synchronized region guarded by the *same* lock. That makes the critical section run as one indivisible step (mutual exclusion).
A synchronized method locks on this (or on the Class object for a static method):
class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
With both methods synchronized on the same this, the increments are now serialized and 2,000 is guaranteed.
synchronized blocks: lock less, lock precisely
Locking a whole method can over-serialize. A synchronized block lets you guard only the critical lines, and lets you choose *which* object's lock to use — usually a private final lock object so no outside code can interfere:
class Bank {
private final Object lock = new Object();
private int balance = 0;
public void deposit(int amt) {
// ... non-critical work can run unlocked ...
synchronized (lock) {
balance += amt; // only THIS is the critical section
}
}
}
The Greenhouse lesson: sync BOTH sides
In Part 2 the project stores shared state (light, water, thermostat) in a shared collection of TwoTuples and writes to it from many event threads through a synchronized setVariable(...). The trap the exam loves: the read side must use the same lock too. If setVariable is synchronized but a getter reads the collection without the lock, the reader can see a half-updated, stale, or torn value. Synchronization protects state only when *every* access — read and write — holds the same lock.
A second classic bug is check-then-act: if (!map.containsKey(k)) map.put(k, v); is two operations; another thread can slip between them. The whole check-then-act must be inside one synchronized region (or use an atomic operation like putIfAbsent).