concurrency-interview: companion code for the Top 40 interview post
Adds the concurrency-interview module: a real race condition with its actual lost-update count, why volatile alone doesn't fix it, a real JVM-detected deadlock (ThreadMXBean.findDeadlockedThreads()) with the fix, and ScopedValue's exact child-thread inheritance rules (finalized in JDK 25 via JEP 506, demonstrated against StructuredTaskScope, still preview per JEP 505/525). 4 runnable demos, 1 JUnit test class (9 tests), 5 captured output transcripts. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
This commit is contained in:
@@ -11,6 +11,7 @@ article; each module's own README has that article's version table, quickstart,
|
||||
| [`vt-pinning`](vt-pinning/) | Diagnosing Virtual Thread Pinning in Production: JFR Events, jcmd, and Real Fixes |
|
||||
| [`synchronizers`](synchronizers/) | CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore in Java |
|
||||
| [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors |
|
||||
| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# concurrency-interview
|
||||
|
||||
Companion code for the ankurm.com post *"Top 40 Java Concurrency Interview Questions and Answers
|
||||
(2026)."* Seventh module in `java-core-examples`, the Java-core / concurrency series - this one
|
||||
backs a hub-style Q&A post rather than a single deep dive.
|
||||
|
||||
Most of the 40 questions in that post link out to the deep, already-verified demos in this repo's
|
||||
other modules (locks, atomics, jmm, synchronizers, executors, vt-pinning) and to a few of this
|
||||
site's existing standalone posts. The four demos here exist because their questions weren't
|
||||
already proven anywhere else in the series: a real race condition and its actual lost-update
|
||||
count, why `volatile` alone doesn't fix it, a real JVM-detected deadlock (and the fix), and exactly
|
||||
which threads a `ScopedValue` binding does and doesn't reach.
|
||||
|
||||
## Versions this was built and tested against
|
||||
|
||||
| Component | Version | Notes |
|
||||
|---|---|---|
|
||||
| JDK | 25.0.4.1+1 (Temurin, LTS) | Every demo needs `--enable-preview` - see below. |
|
||||
| JUnit Jupiter | 5.11.0 | Correctness tests, 5 repeats for the one timing-sensitive case. |
|
||||
| Maven | 3.9.11 | |
|
||||
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. |
|
||||
|
||||
**Why `--enable-preview` for everything, even the demos that don't touch a preview API:** this
|
||||
module is compiled as one unit, and `ScopedValueDemo` needs `StructuredTaskScope` (still preview
|
||||
in JDK 25/26, per JEP 505/525) to demonstrate scoped-value inheritance into a child thread.
|
||||
`ScopedValue` itself was finalized in JDK 25 via JEP 506 and needs no flag on its own.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/path/to/jdk-25
|
||||
mvn package
|
||||
java --enable-preview -cp target/classes com.ankurm.concurrencyinterview.RaceConditionDemo
|
||||
java --enable-preview -cp target/classes com.ankurm.concurrencyinterview.DeadlockDemo
|
||||
```
|
||||
|
||||
`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME`).
|
||||
|
||||
## What's in here
|
||||
|
||||
| File | What it shows |
|
||||
|---|---|
|
||||
| `.../RaceConditionDemo.java` | A plain `int` incremented by 8 threads, 200,000 times each, next to an `AtomicInteger` doing identical work - the real lost-update count, not an assertion that one exists. |
|
||||
| `.../VolatileNotEnoughDemo.java` | The same workload against a `volatile int` - visibility is guaranteed, atomicity is not, and the loss is just as real. |
|
||||
| `.../DeadlockDemo.java` | Two threads, two locks, acquired in opposite order - a genuine deadlock, detected with `ThreadMXBean.findDeadlockedThreads()`, then a second run with consistent lock ordering that completes normally. |
|
||||
| `.../ScopedValueDemo.java` | `ScopedValue` visible to same-thread direct and indirect callees; NOT inherited by a plain `new Thread()`; inherited by a thread forked from `StructuredTaskScope`. |
|
||||
| `src/test/.../InterviewClaimsTest.java` | Asserts what's actually guaranteed: `AtomicInteger` never loses an update (5 repeats), and `ScopedValue`'s exact inheritance rule - deliberately does NOT assert on the raw-int or volatile-int race outcomes, since those are genuinely non-deterministic. |
|
||||
| `output/01`-`04` | Each demo's real run. |
|
||||
| `output/05` | JUnit correctness run. |
|
||||
|
||||
## Reading the results honestly (2-vCPU sandbox)
|
||||
|
||||
**The race condition lost 351,504 of 1,600,000 increments this run** (`output/01`) - not "some"
|
||||
or "a few," an exact, real number from an actual run, which is the whole point of not just
|
||||
asserting that `count++` is unsafe. Rerun it and the number will be different; it won't be zero on
|
||||
this hardware with this workload.
|
||||
|
||||
**`volatile` lost 744,982 updates under the identical workload** (`output/02`). `volatile`
|
||||
guarantees every thread observes the latest write and prevents instruction reordering around it -
|
||||
it says nothing about two threads reading the same value before either writes back, which is
|
||||
exactly what `count++` (read, add, write) allows. The fix for a shared counter is `AtomicInteger`
|
||||
or a lock, not the `volatile` keyword.
|
||||
|
||||
**The deadlock in `output/03` is real, not simulated** - `ThreadMXBean.findDeadlockedThreads()` is
|
||||
the same API a monitoring agent or `jstack` uses, and it reports both threads, each blocked on the
|
||||
lock the other holds. The fix demonstrated right after it isn't "add more locking" - it's ensuring
|
||||
every thread that needs both locks acquires them in the same order, which makes the circular-wait
|
||||
condition structurally impossible.
|
||||
|
||||
**`ScopedValue` inheritance in `output/04` is exactly as narrow as the JDK docs say, confirmed
|
||||
rather than assumed:** bound on the main thread, visible to nested method calls on that same
|
||||
thread; a plain `new Thread()` reports `isBound() = false`; only the `StructuredTaskScope.fork()`'d
|
||||
thread sees the binding. Reaching for `ScopedValue` and expecting it to "just work" across a
|
||||
hand-rolled `new Thread()` is a real interview trap, not a hypothetical one.
|
||||
|
||||
## License
|
||||
|
||||
MIT - see the [repo-wide LICENSE](../LICENSE).
|
||||
@@ -0,0 +1,5 @@
|
||||
expected increments: 1600000
|
||||
unsafeCounter (plain int, unsynchronized): 1248496
|
||||
safeCounter (AtomicInteger): 1600000
|
||||
unsafeCounter lost 351504 updates to the race
|
||||
safeCounter matches expected: true
|
||||
@@ -0,0 +1,5 @@
|
||||
expected increments: 1600000
|
||||
volatileCounter: 855018 (volatile guarantees every thread SEES the latest value - it does not make ++ atomic)
|
||||
atomicCounter: 1600000
|
||||
volatileCounter lost updates: 744982
|
||||
atomicCounter matches expected: true
|
||||
@@ -0,0 +1,12 @@
|
||||
=== Opposite lock order: a real deadlock, detected via ThreadMXBean ===
|
||||
thread-1: holding lockA, waiting for lockB
|
||||
thread-2: holding lockB, waiting for lockA
|
||||
main: findDeadlockedThreads() detected 2 deadlocked threads
|
||||
thread-1 is blocked on java.lang.Object@5451c3a8, owned by thread-2
|
||||
thread-2 is blocked on java.lang.Object@49476842, owned by thread-1
|
||||
|
||||
=== Same lock order on both threads: no deadlock possible ===
|
||||
thread-1 (fixed order): acquired both locks, done
|
||||
thread-2 (fixed order): acquired both locks, done
|
||||
main: both fixed-order threads finished, isAlive t1=false t2=false
|
||||
done (the process exits here - the two deadlocked threads above never finish)
|
||||
@@ -0,0 +1,13 @@
|
||||
=== Same-thread binding: visible to direct AND indirect callees ===
|
||||
handleRequest(): REQUEST_ID.get() = req-42
|
||||
logSomewhereDeeper() (indirect callee, same thread): REQUEST_ID.get() = req-42
|
||||
|
||||
=== Outside any binding: isBound() is false ===
|
||||
main: REQUEST_ID.isBound() = false
|
||||
|
||||
=== A plain `new Thread()` does NOT inherit the binding ===
|
||||
plain child thread: REQUEST_ID.isBound() = false
|
||||
|
||||
=== A StructuredTaskScope.fork()'d thread DOES inherit the binding ===
|
||||
forked subtask: REQUEST_ID.isBound() = true, REQUEST_ID.get() = req-100
|
||||
done
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.ankurm.concurrencyinterview.InterviewClaimsTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.216 s -- in com.ankurm.concurrencyinterview.InterviewClaimsTest
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>java-core-examples</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>concurrency-interview</artifactId>
|
||||
<name>concurrency-interview</name>
|
||||
<description>Verified demos behind the "Top 40 Java Concurrency Interview Questions" hub post: a real race condition, a real detected deadlock, volatile's actual limits, and ScopedValue inheritance rules.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>25</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<argLine>--enable-preview</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in ../output/. Requires JDK25_HOME. Every demo in this module needs
|
||||
# --enable-preview to run (ScopedValueDemo uses StructuredTaskScope, still preview in JDK 25/26),
|
||||
# even though ScopedValue itself is finalized (JEP 506).
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${JDK25_HOME:-}" ]]; then
|
||||
echo "JDK25_HOME must be set (e.g. /path/to/jdk-25)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=output
|
||||
mkdir -p "$OUT"
|
||||
|
||||
JAVA_HOME="$JDK25_HOME" mvn -q -f ../pom.xml -pl concurrency-interview package
|
||||
|
||||
run() {
|
||||
local class=$1
|
||||
local outfile=$2
|
||||
echo "==> $class"
|
||||
"$JDK25_HOME/bin/java" --enable-preview -cp target/classes "com.ankurm.concurrencyinterview.$class" 2>&1 | grep -v "Picked up" > "$OUT/$outfile"
|
||||
}
|
||||
|
||||
run RaceConditionDemo 01-race-condition.txt
|
||||
run VolatileNotEnoughDemo 02-volatile-not-enough.txt
|
||||
run DeadlockDemo 03-deadlock-detection.txt
|
||||
run ScopedValueDemo 04-scoped-value-inheritance.txt
|
||||
|
||||
cp target/surefire-reports/com.ankurm.concurrencyinterview.InterviewClaimsTest.txt "$OUT/05-correctness-tests.txt"
|
||||
|
||||
echo "Done. See $OUT/"
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.ankurm.concurrencyinterview;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.ThreadInfo;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
|
||||
/**
|
||||
* The textbook deadlock: two threads, two locks, acquired in opposite order. Thread-1 takes
|
||||
* lockA then reaches for lockB; Thread-2 takes lockB then reaches for lockA. Neither can proceed
|
||||
* and neither will ever release what it's already holding. This demo doesn't just describe that -
|
||||
* it triggers a real deadlock, detects it with the same java.lang.management API a monitoring
|
||||
* tool would use (ThreadMXBean.findDeadlockedThreads()), and then runs a second, fixed version
|
||||
* that acquires both locks in the same global order every time, which completes normally.
|
||||
*/
|
||||
public class DeadlockDemo {
|
||||
|
||||
private static final Object lockA = new Object();
|
||||
private static final Object lockB = new Object();
|
||||
|
||||
// Separate locks for the fixed-order demo: the two threads above deadlock forever and never
|
||||
// release lockA/lockB, so reusing those objects here would just hang a second time.
|
||||
private static final Object lockC = new Object();
|
||||
private static final Object lockD = new Object();
|
||||
|
||||
private static void triggerDeadlock() throws InterruptedException {
|
||||
Thread t1 = new Thread(() -> {
|
||||
synchronized (lockA) {
|
||||
System.out.println("thread-1: holding lockA, waiting for lockB");
|
||||
sleep(200);
|
||||
synchronized (lockB) {
|
||||
System.out.println("thread-1: this line never prints - deadlocked before reaching it");
|
||||
}
|
||||
}
|
||||
}, "thread-1");
|
||||
|
||||
Thread t2 = new Thread(() -> {
|
||||
synchronized (lockB) {
|
||||
System.out.println("thread-2: holding lockB, waiting for lockA");
|
||||
sleep(200);
|
||||
synchronized (lockA) {
|
||||
System.out.println("thread-2: this line never prints - deadlocked before reaching it");
|
||||
}
|
||||
}
|
||||
}, "thread-2");
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
|
||||
Thread.sleep(1000); // give the deadlock time to actually form
|
||||
|
||||
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
|
||||
long[] deadlockedIds = threadBean.findDeadlockedThreads();
|
||||
if (deadlockedIds == null) {
|
||||
System.out.println("main: findDeadlockedThreads() found nothing - unexpected for this demo");
|
||||
return;
|
||||
}
|
||||
System.out.println("main: findDeadlockedThreads() detected " + deadlockedIds.length + " deadlocked threads");
|
||||
for (ThreadInfo info : threadBean.getThreadInfo(deadlockedIds, true, true)) {
|
||||
System.out.println(" " + info.getThreadName() + " is blocked on " + info.getLockInfo()
|
||||
+ ", owned by " + info.getLockOwnerName());
|
||||
}
|
||||
// The two threads above are stuck forever - this JVM process exits without joining them.
|
||||
}
|
||||
|
||||
private static void runFixedOrderedVersion() throws InterruptedException {
|
||||
Thread t1 = new Thread(() -> {
|
||||
synchronized (lockC) {
|
||||
sleep(100);
|
||||
synchronized (lockD) {
|
||||
System.out.println("thread-1 (fixed order): acquired both locks, done");
|
||||
}
|
||||
}
|
||||
}, "thread-1-fixed");
|
||||
|
||||
Thread t2 = new Thread(() -> {
|
||||
synchronized (lockC) { // same order as thread-1: lockC first, lockD second
|
||||
sleep(100);
|
||||
synchronized (lockD) {
|
||||
System.out.println("thread-2 (fixed order): acquired both locks, done");
|
||||
}
|
||||
}
|
||||
}, "thread-2-fixed");
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
t1.join(5000);
|
||||
t2.join(5000);
|
||||
System.out.println("main: both fixed-order threads finished, isAlive t1=" + t1.isAlive()
|
||||
+ " t2=" + t2.isAlive());
|
||||
}
|
||||
|
||||
private static void sleep(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
System.out.println("=== Opposite lock order: a real deadlock, detected via ThreadMXBean ===");
|
||||
triggerDeadlock();
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Same lock order on both threads: no deadlock possible ===");
|
||||
runFixedOrderedVersion();
|
||||
|
||||
System.out.println("done (the process exits here - the two deadlocked threads above never finish)");
|
||||
System.exit(0); // the deadlocked threads from triggerDeadlock() would otherwise hang the JVM forever
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.concurrencyinterview;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* The canonical interview answer to "why isn't count++ thread-safe?" is that it's a
|
||||
* read-modify-write sequence, not one atomic step. This demo doesn't just assert that - it runs
|
||||
* 8 threads each incrementing a plain int 200,000 times (1,600,000 total increments expected) and
|
||||
* prints whatever the real, lost-update total actually comes out to, side by side with an
|
||||
* AtomicInteger doing the identical work correctly. The exact deficit is inherently
|
||||
* non-deterministic - that's the point being demonstrated, not a bug in the demo.
|
||||
*/
|
||||
public class RaceConditionDemo {
|
||||
|
||||
private static int unsafeCounter = 0;
|
||||
private static final AtomicInteger safeCounter = new AtomicInteger(0);
|
||||
|
||||
private static final int THREADS = 8;
|
||||
private static final int INCREMENTS_PER_THREAD = 200_000;
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread[] threads = new Thread[THREADS];
|
||||
for (int i = 0; i < THREADS; i++) {
|
||||
threads[i] = new Thread(() -> {
|
||||
for (int j = 0; j < INCREMENTS_PER_THREAD; j++) {
|
||||
unsafeCounter++; // read-modify-write, not atomic
|
||||
safeCounter.incrementAndGet(); // genuinely atomic
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Thread t : threads) t.start();
|
||||
for (Thread t : threads) t.join();
|
||||
|
||||
int expected = THREADS * INCREMENTS_PER_THREAD;
|
||||
System.out.println("expected increments: " + expected);
|
||||
System.out.println("unsafeCounter (plain int, unsynchronized): " + unsafeCounter);
|
||||
System.out.println("safeCounter (AtomicInteger): " + safeCounter.get());
|
||||
if (unsafeCounter != expected) {
|
||||
System.out.println("unsafeCounter lost " + (expected - unsafeCounter) + " updates to the race");
|
||||
} else {
|
||||
System.out.println("unsafeCounter happened to come out correct this run - the race is still real, "
|
||||
+ "just didn't lose anything THIS time (rerun it - it usually does on this box)");
|
||||
}
|
||||
System.out.println("safeCounter matches expected: " + (safeCounter.get() == expected));
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.ankurm.concurrencyinterview;
|
||||
|
||||
import java.util.concurrent.StructuredTaskScope;
|
||||
|
||||
/**
|
||||
* ScopedValue (java.lang.ScopedValue) was finalized in JDK 25 via JEP 506 - no preview flag
|
||||
* needed for the basic API. But its main selling point over ThreadLocal is sharing immutable
|
||||
* context with CHILD threads, and that inheritance is NOT automatic for just any thread: it only
|
||||
* happens for threads forked from a StructuredTaskScope, which is still a preview API in JDK 25/26
|
||||
* (JEP 505/525). This class needs --enable-preview for that reason alone - the ScopedValue calls
|
||||
* themselves wouldn't require it.
|
||||
*/
|
||||
public class ScopedValueDemo {
|
||||
|
||||
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
|
||||
|
||||
private static void handleRequest() {
|
||||
System.out.println("handleRequest(): REQUEST_ID.get() = " + REQUEST_ID.get());
|
||||
logSomewhereDeeper();
|
||||
}
|
||||
|
||||
private static void logSomewhereDeeper() {
|
||||
System.out.println("logSomewhereDeeper() (indirect callee, same thread): REQUEST_ID.get() = " + REQUEST_ID.get());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
System.out.println("=== Same-thread binding: visible to direct AND indirect callees ===");
|
||||
ScopedValue.where(REQUEST_ID, "req-42").run(ScopedValueDemo::handleRequest);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Outside any binding: isBound() is false ===");
|
||||
System.out.println("main: REQUEST_ID.isBound() = " + REQUEST_ID.isBound());
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== A plain `new Thread()` does NOT inherit the binding ===");
|
||||
ScopedValue.where(REQUEST_ID, "req-99").run(() -> {
|
||||
Thread plainChild = new Thread(() ->
|
||||
System.out.println("plain child thread: REQUEST_ID.isBound() = " + REQUEST_ID.isBound()));
|
||||
plainChild.start();
|
||||
try {
|
||||
plainChild.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== A StructuredTaskScope.fork()'d thread DOES inherit the binding ===");
|
||||
ScopedValue.where(REQUEST_ID, "req-100").run(() -> {
|
||||
try (var scope = StructuredTaskScope.open()) {
|
||||
scope.fork(() -> {
|
||||
System.out.println("forked subtask: REQUEST_ID.isBound() = " + REQUEST_ID.isBound()
|
||||
+ ", REQUEST_ID.get() = " + REQUEST_ID.get());
|
||||
return null;
|
||||
});
|
||||
scope.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
System.out.println("done");
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.ankurm.concurrencyinterview;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A common interview trap: "just mark it volatile" does NOT make count++ thread-safe. volatile
|
||||
* guarantees visibility (every thread sees the latest write) and ordering, but it does nothing
|
||||
* about the read-modify-write sequence being non-atomic - two threads can both read the same
|
||||
* value before either writes back. This runs a volatile int through the identical concurrent
|
||||
* increment workload as RaceConditionDemo, next to an AtomicInteger, to show the failure mode is
|
||||
* the same as a plain int, not fixed by the keyword alone.
|
||||
*/
|
||||
public class VolatileNotEnoughDemo {
|
||||
|
||||
private static volatile int volatileCounter = 0;
|
||||
private static final AtomicInteger atomicCounter = new AtomicInteger(0);
|
||||
|
||||
private static final int THREADS = 8;
|
||||
private static final int INCREMENTS_PER_THREAD = 200_000;
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread[] threads = new Thread[THREADS];
|
||||
for (int i = 0; i < THREADS; i++) {
|
||||
threads[i] = new Thread(() -> {
|
||||
for (int j = 0; j < INCREMENTS_PER_THREAD; j++) {
|
||||
volatileCounter++; // visibility guaranteed, atomicity is NOT
|
||||
atomicCounter.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Thread t : threads) t.start();
|
||||
for (Thread t : threads) t.join();
|
||||
|
||||
int expected = THREADS * INCREMENTS_PER_THREAD;
|
||||
System.out.println("expected increments: " + expected);
|
||||
System.out.println("volatileCounter: " + volatileCounter
|
||||
+ " (volatile guarantees every thread SEES the latest value - it does not make ++ atomic)");
|
||||
System.out.println("atomicCounter: " + atomicCounter.get());
|
||||
System.out.println("volatileCounter lost updates: " + (expected - volatileCounter));
|
||||
System.out.println("atomicCounter matches expected: " + (atomicCounter.get() == expected));
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.ankurm.concurrencyinterview;
|
||||
|
||||
import org.junit.jupiter.api.RepeatedTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.StructuredTaskScope;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Deliberately does NOT assert on the raw-int or volatile-int race demos - whether a given run
|
||||
* happens to lose an update is inherently non-deterministic, and a test that occasionally passes
|
||||
* by luck is worse than no test. What's asserted here is what's actually guaranteed: AtomicInteger
|
||||
* never loses an update under the identical workload, and ScopedValue's inheritance rule is exactly
|
||||
* what the docs say it is, not "roughly."
|
||||
*/
|
||||
class InterviewClaimsTest {
|
||||
|
||||
@RepeatedTest(5)
|
||||
void atomicIntegerNeverLosesAnUpdateUnderTheSameWorkloadThatBreaksAPlainInt() throws InterruptedException {
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
int threads = 8;
|
||||
int incrementsPerThread = 50_000;
|
||||
Thread[] pool = new Thread[threads];
|
||||
for (int i = 0; i < threads; i++) {
|
||||
pool[i] = new Thread(() -> {
|
||||
for (int j = 0; j < incrementsPerThread; j++) {
|
||||
counter.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Thread t : pool) t.start();
|
||||
for (Thread t : pool) t.join();
|
||||
assertEquals(threads * incrementsPerThread, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void scopedValueIsVisibleToDirectAndIndirectCalleesOnTheSameThread() {
|
||||
ScopedValue<String> value = ScopedValue.newInstance();
|
||||
AtomicReference<String> seenByCallee = new AtomicReference<>();
|
||||
ScopedValue.where(value, "abc").run(() -> seenByCallee.set(value.get()));
|
||||
assertEquals("abc", seenByCallee.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void scopedValueGetOutsideAnyBindingThrows() {
|
||||
ScopedValue<String> value = ScopedValue.newInstance();
|
||||
assertFalse(value.isBound());
|
||||
assertThrows(NoSuchElementException.class, value::get);
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainThreadDoesNotInheritAScopedValueBinding() throws InterruptedException {
|
||||
ScopedValue<String> value = ScopedValue.newInstance();
|
||||
AtomicReference<Boolean> boundInChild = new AtomicReference<>();
|
||||
ScopedValue.where(value, "parent-bound").run(() -> {
|
||||
Thread child = new Thread(() -> boundInChild.set(value.isBound()));
|
||||
child.start();
|
||||
try {
|
||||
child.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
assertEquals(false, boundInChild.get(), "a plain new Thread() must NOT inherit the binding");
|
||||
}
|
||||
|
||||
@Test
|
||||
void structuredTaskScopeForkedThreadDoesInheritTheBinding() throws Exception {
|
||||
ScopedValue<String> value = ScopedValue.newInstance();
|
||||
AtomicReference<String> seenInSubtask = new AtomicReference<>();
|
||||
ScopedValue.where(value, "inherited").run(() -> {
|
||||
try (var scope = StructuredTaskScope.open()) {
|
||||
scope.fork(() -> {
|
||||
seenInSubtask.set(value.get());
|
||||
return null;
|
||||
});
|
||||
scope.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
assertEquals("inherited", seenInSubtask.get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user