Skip to main content

Week 8 Day 2 — synchronized, Lock, and volatile

Goal

Today I want three tools with different jobs: exclude, publish, atomic update.

Main questions:

  1. What does synchronized give me?
  2. When do I use ReentrantLock?
  3. What does volatile actually guarantee?
  4. Why is volatile int c; c++ still a race?
  5. What are AtomicInteger and LongAdder for?

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. volatile only publishes. ++ needs a lock or an atomic.


2. ReentrantLock

Same exclusion idea, extra policies:

  • tryLock, timed lock
  • interruptible lock
  • optional fairness
  • multiple Condition objects (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 synchronized an entire @Service method. I would single-thread the app.
  • Metrics: Micrometer / LongAdder, not a locked int on the service.
  • volatile for a feature flag read by many threads, written rarely.
  • Database transactions are the lock for business invariants (placed counts belong in SQL), not synchronized on 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. volatile only publishes. ++ needs a lock or an atomic.

Lock a private final object. Keep the critical section tiny.

AtomicInteger is for one counter. Two fields together still need a lock.

Next: Week 8 Day 3 — Happens-before and Safe Publication