Week 8 Day 2 — synchronized, Lock, and volatile
Goal
Today I want three tools with different jobs: exclude, publish, atomic update.
Main questions:
- What does
synchronizedgive me? - When do I use
ReentrantLock? - What does
volatileactually guarantee? - Why is
volatile int c; c++still a race? - What are
AtomicIntegerandLongAdderfor?
1. synchronized — mutual exclusion and a barrier
synchronized is a monitor. One thread at a time in the critical section on that lock object. Entering and exiting also create a happens-before edge (Day 3): unlock’s writes become visible to the next lock.
private final Object lock = new Object();
private int placed;
public void place() {
synchronized (lock) {
placed++;
// tiny critical section
}
}
I lock a private final object, not this of a public class (callers could synchronize on my service and deadlock with me). I keep the section tiny: no HTTP, no long DB calls inside synchronized on a singleton — that serializes all requests.
Reentrant: the same thread can acquire the same monitor again. Nested calls on the same lock do not self-deadlock.
synchronized method is synchronized (this). I prefer an explicit private lock.
Memory sentence:
Locks exclude and publish.
volatileonly publishes.++needs a lock or an atomic.
2. ReentrantLock
Same exclusion idea, extra policies:
tryLock, timed lock- interruptible lock
- optional fairness
- multiple
Conditionobjects (vs one wait-set per monitor)
private final ReentrantLock lock = new ReentrantLock();
public void place() {
lock.lock();
try {
placed++;
} finally {
lock.unlock();
}
}
Always unlock in finally. If I need try-lock or interruptibility, I pick ReentrantLock. Otherwise synchronized is simpler and enough.
ReadWriteLock is for many readers, rare writers. Measure; it is not automatically faster.
3. volatile — visibility, not exclusion
A volatile write happens-before a subsequent read of that same variable. The reader sees that write and, transitively, writes before it in the writing thread.
private volatile boolean running = true;
public void stop() {
running = false;
}
public void loop() {
while (running) {
work();
}
}
A stop flag is the textbook use.
volatile int c; c++ is still three steps (read, add, write). Two threads interleave. Visibility of each write does not make the compound action atomic.
volatile does not make a HashMap thread-safe. Publishing the reference to an immutable map can be safe; mutating the map after publish is not.
4. Atomics
AtomicInteger.incrementAndGet() is a CAS loop: atomic ++ without holding a monitor.
private final AtomicInteger placed = new AtomicInteger();
public void place() {
placed.incrementAndGet();
}
Good for independent counters. Under high contention, LongAdder spreads stripes and sums them — better for hot metrics.
Atomics do not protect two fields that must change together. That is a lock (or one immutable object published with volatile / AtomicReference).
5. Spring connection
- I almost never
synchronizedan entire@Servicemethod. I would single-thread the app. - Metrics: Micrometer /
LongAdder, not a lockedinton the service. volatilefor a feature flag read by many threads, written rarely.- Database transactions are the lock for business invariants (
placedcounts belong in SQL), notsynchronizedon the bean.
Deadlock sketch: thread A holds lock on OrderService, calls InventoryService; thread B holds InventoryService, calls OrderService. Nested @Transactional plus synchronized beans is a way to get there. Prefer no locks on services; use DB locking.
6. Common traps
Trap 1: volatile as if it were a lock.
Trap 2: synchronized (this) on a public singleton.
Trap 3: Long I/O inside synchronized.
Trap 4: AtomicInteger for a check-then-act of two fields.
Trap 5: Forgetting unlock() when using ReentrantLock.
Practice Questions and Answers
Question 1
synchronized vs ReentrantLock vs volatile?
Answer:
synchronized is a monitor: exclusion plus a memory barrier. ReentrantLock is the same idea with try-lock, timed and interruptible acquire, and finally unlock. volatile is visibility and ordering for that variable, not mutual exclusion and not atomic ++.
Question 2
Why is volatile int c; c++ a race?
Answer:
++ is read, add, write. volatile makes each write visible; it does not make the three steps one atomic action. Two threads can both read the same value. Use AtomicInteger or a lock.
Question 3
What does reentrant mean?
Answer:
The thread that already holds the lock may acquire it again. Nested calls on the same monitor do not block forever. The lock is released when the matching unlock count hits zero.
Question 4
When do I pick AtomicInteger over synchronized?
Answer:
A single independent counter with no other fields in the same invariant. If I must update two fields together, I need one lock (or one atomic reference to an immutable object).
Question 5
Should a Spring @Service be synchronized?
Answer:
Not the whole class. That serializes every request. Keep the service stateless. If I have a tiny in-memory critical section, lock a private object around only those lines — or use the database as the source of truth.
Memory sentences
Locks exclude and publish.
volatileonly publishes.++needs a lock or an atomic.
Lock a private final object. Keep the critical section tiny.
AtomicIntegeris for one counter. Two fields together still need a lock.