Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java race interview: deterministic lost updates and atomic per-key changes

Last updated: 29 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A lost update occurs when two operations read the same prior value and later overwrite one another’s computed changes.

Download Java source kit

Java 8+. The program uses JDK classes and requires no preview flags.

Produce the interleaving explicitly

Two workers each subtract three units from a stock value of seven. The first phase waits until both workers have read seven before allowing either to write. Both then publish four. Each map call is individually safe, but one subtraction has disappeared.

The latches define the problematic interleaving. A sleep-based demonstration might pass or fail depending on scheduler timing and would not prove that both reads preceded both writes. The worker futures also provide a completion boundary before the result is inspected.

The corrected phase uses compute for each key update. That operation expresses the per-key transformation as one map operation. It still does not make a second map or an external message part of the same atomic boundary.

Explain failure and ownership

An interview answer should distinguish thread-safe storage from a valid application protocol. It should also account for worker failure and shutdown. The program bounds its waits, observes future results and closes its executor; it does not leave hidden background work running after the example finishes.

Working program

Java
import java.util.concurrent.*;
public class LostStockUpdateTrace {
    public static void main(String[] args)throws Exception{
        ConcurrentHashMap<String,Integer> stock=new ConcurrentHashMap<>();stock.put("PACK",7);
        ExecutorService pool=Executors.newFixedThreadPool(2);CountDownLatch read=new CountDownLatch(2),publish=new CountDownLatch(1);
        try{
            Callable<Void> broken=()->{int prior=stock.get("PACK");read.countDown();if(!publish.await(2,TimeUnit.SECONDS))throw new IllegalStateException("Publish wait");stock.put("PACK",prior-3);return null;};
            Future<Void> first=pool.submit(broken),second=pool.submit(broken);
            if(!read.await(2,TimeUnit.SECONDS))throw new IllegalStateException("Read wait");publish.countDown();first.get(2,TimeUnit.SECONDS);second.get(2,TimeUnit.SECONDS);System.out.println(stock.get("PACK"));
            stock.put("PACK",7);Runnable corrected=()->stock.compute("PACK",(key,prior)->prior-3);
            Future<?> a=pool.submit(corrected),b=pool.submit(corrected);a.get(2,TimeUnit.SECONDS);b.get(2,TimeUnit.SECONDS);System.out.println(stock.get("PACK"));
        }finally{publish.countDown();pool.shutdownNow();if(!pool.awaitTermination(2,TimeUnit.SECONDS))throw new IllegalStateException("Workers alive");}
    }
}

Output

Output
4
1

Costs and boundaries

The trace has a fixed two-worker state. Map updates have their implementation costs plus contention; the latches deliberately wait for a specified interleaving. The corrected arithmetic assumes the prior value exists and the requested subtraction is valid. A real stock rule must check availability inside the atomic operation.

Common Mistakes

  • Thread-safe get and put do not combine into an atomic transformation.
  • Sleeping does not prove the intended interleaving happened.
  • Per-key compute cannot commit an external side effect.

Read next

Several structures, Atomic map operations.

java
transaction-races-interview
Storage details