← All writing

FIELD NOTES

Java Concurrency: Race Conditions, ConcurrentHashMap and CountDownLatch

In this note

Concurrency bugs are difficult because correctness depends on timing. I experimented with three Java building blocks: conditional updates in ConcurrentHashMap, coordination with CountDownLatch, and the failure mode that occurs when a latch is never released.

1. Avoid check-then-act updates

A thread-safe map does not make a sequence of separate operations atomic. This pattern has a race: another thread can change the value after get() and before put().

1
2
3
4
// Not atomic as a compound operation
if ("Cabbage".equals(map.get(3))) {
    map.put(3, "Charlie");
}

ConcurrentHashMap provides a conditional replace operation. It updates the mapping only if the current value matches the expected value, and returns whether the replacement happened. This is CAS-like at the API level; it is safer to describe it as an atomic conditional update rather than assume every call maps to one specific hardware instruction.

1
boolean updated = map.replace(3, "Cabbage", "Charlie");

2. start(), join() and CountDownLatch solve different problems

Calling t1.start() and then t2.start() starts two independent threads; it does not guarantee which worker runs first. Calling t1.join() makes the calling thread wait for t1 to finish. It does not make t2 wait for t1.

Use a CountDownLatch when one thread must wait for an event signalled by another thread. In this example, worker B waits until worker A has completed its update and decremented the latch.

1
2
3
4
Worker A: update shared state -> countDown()
                                      |
                                      v
Worker B:                  await() -> continue

Actions in a thread before countDown() happen-before actions following a corresponding successful await() in another thread. The latch is a one-shot coordination mechanism; it does not make unrelated shared state thread-safe by itself.

3. Complete example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class ConcurrencyDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<Integer, String> values = new ConcurrentHashMap<>();
        values.put(3, "Cabbage");

        CountDownLatch firstWorkerDone = new CountDownLatch(1);

        Thread workerA = new Thread(() -> {
            try {
                boolean changed = values.replace(3, "Cabbage", "Charlie");
                System.out.println("A changed value: " + changed);
            } finally {
                // Release the dependent worker even if the task fails.
                firstWorkerDone.countDown();
            }
        });

        Thread workerB = new Thread(() -> {
            try {
                if (!firstWorkerDone.await(2, TimeUnit.SECONDS)) {
                    System.out.println("B timed out waiting for A");
                    return;
                }

                boolean changed = values.replace(3, "Cabbage", "Chord");
                System.out.println("B changed value: " + changed);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        workerA.start();
        workerB.start();
        workerA.join();
        workerB.join();

        System.out.println("Final value: " + values.get(3));
    }
}

Worker A changes Cabbage to Charlie. After the latch opens, worker B tries to replace Cabbage; that conditional update returns false because the value is now Charlie.

4. wait() vs sleep() and latch timeouts

  • Thread.sleep() pauses the current thread without releasing any monitor it owns.
  • Object.wait() must be called while holding that object’s monitor. It releases that monitor while waiting and can be resumed by notification, interruption or timeout.
  • If a worker throws before calling countDown(), another worker can wait forever. Put the countdown in a finally block when completion should always release the latch, and consider await(timeout, unit) when an indefinite wait is unacceptable.

What I took away

Use atomic map operations for compound updates, use CountDownLatch to signal a one-time event, and make failure paths part of the concurrency design. Starting threads does not establish execution order.