A lost update occurs when two operations read the same prior value and later overwrite one another’s computed changes.
Java race interview: deterministic lost updates and atomic per-key changes
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
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
4
1Costs 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.
