Skip to content

Step 1 of 7 · Reading · ~4 min

Learn

Concurrency

Threads and the Memory Model

In Java Intermediate you started threads, joined them, and fixed a lost-update race with AtomicInteger. This lesson explains why those fixes work, and teaches the tool you have not used yet: making one thread wait until another has changed something.

A write is not automatically seen

Each core may keep a variable in a register or cache. Without a rule saying otherwise, the JVM is free to let a reader keep using a stale copy. This program can hang forever:

static boolean ready = false;           // plain field
static int config = 0;

// thread A                              // thread B
config = 42;                             while (!ready) { }   // spins
ready = true;                            System.out.println(config);

The JIT may read ready once and reuse it, so B never leaves the loop. Even if it does, it could print 0, because nothing forces config = 42 to become visible before ready = true. Declaring ready as volatile fixes both problems: B's loop sees the new value, and every write A made before the volatile write is visible to B after the volatile read.

Happens-before: the only promise you get

The Java Memory Model does not talk about "latest value". It defines happens-before edges; if action X happens-before action Y, then Y sees everything X wrote. The edges you will use:

  • a write to a volatile field, then a later read of that same field
  • releasing a monitor (leaving synchronized), then a later acquire of the same monitor
  • Thread.start(), then everything the new thread does
  • everything a thread does, then join() returning in another thread

No edge, no guarantee. That is why a result array filled by a worker can be read safely after join() without any extra locking.

Atomic does not mean visible, and visible does not mean atomic

volatile int n; n++ is still read, add, write: two threads can both read 7 and both write 8. For a read-modify-write you need either a lock or a compare-and-set (CAS) loop: read the current value, compute the new one, and store it only if nobody changed the field in between; otherwise retry.

static final AtomicInteger sold = new AtomicInteger();

static boolean reserve(int limit) {      // ticket office with a hard limit
    while (true) {
        int cur = sold.get();
        if (cur >= limit) return false;
        if (sold.compareAndSet(cur, cur + 1)) return true;   // else someone beat us; retry
    }
}

Waiting for a condition: wait and notifyAll

Spinning on a flag burns a core. A thread that needs a condition to become true should sleep on a monitor and be woken when the condition may have changed:

class Latch {
    private boolean open = false;
    synchronized void await() throws InterruptedException {
        while (!open) wait();            // releases the monitor while asleep
    }
    synchronized void release() {
        open = true;
        notifyAll();                     // wake every sleeper so it re-checks
    }
}

Three rules make this correct. wait, notify and notifyAll must be called while holding the object's monitor, or you get IllegalMonitorStateException. The test sits in a while, never an if, because a woken thread must re-check: another thread may have run first, and spurious wakeups exist. And when threads wait for different conditions on the same monitor, use notifyAll; a single notify can wake the wrong one and leave the right one asleep forever.

Common mistakes

  • Making a counter volatile and calling n++. Updates are lost and the total comes out below the expected one.
  • if (!open) wait(); A thread that wakes with the condition still false carries on and reads garbage.
  • notify() where producers and consumers share one monitor. The wakeup can go to a thread of the wrong kind, and the program hangs with every thread waiting.
  • Calling wait() outside synchronized. It throws IllegalMonitorStateException immediately.

Your exercise

Finish a fixed-capacity buffer shared by one producer thread and one consumer thread: put must block while it is full and take must block while it is empty. The storage and the two threads are already written; the most likely mistake is a method that works on a quiet run but never wakes its partner, so the program hangs.

Up nextExecutors and CompletableFutureConcurrency

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…