False Sharing and @Contended
False sharing is a hidden performance problem in multithreaded code. CPUs move memory between main memory and their caches in blocks called cache lines, usually 64 bytes. If two threads on different cores keep writing different variables that happen to sit on the same cache line, every write forces the other core to throw away its copy of the whole line and fetch it again. The threads don't share any data, yet they slow each other down as if they did. In this lesson, you will see false sharing in a benchmark and learn how padding and @Contended fix it.
How False Sharing Happens
Example: Measuring False Sharing
Let's see an example where two threads each increment their own counter 20 million times. The counters live in an AtomicLongArray, so every increment really writes to memory (a plain long[] loop would be optimized into a single write by the JIT compiler, hiding the effect). In the first version the two counters are next to each other in an array; in the second they are 16 longs (128 bytes) apart. Times depend on the computer, so the output is a sample.
import java.util.concurrent.atomic.AtomicLongArray;
public class FalseSharing {
static final AtomicLongArray counters = new AtomicLongArray(32);
static long run(int indexA, int indexB) throws InterruptedException {
counters.set(indexA, 0);
counters.set(indexB, 0);
Thread a = new Thread(() -> {
for (int i = 0; i < 20_000_000; i++) {
counters.incrementAndGet(indexA);
}
});
Thread b = new Thread(() -> {
for (int i = 0; i < 20_000_000; i++) {
counters.incrementAndGet(indexB);
}
});
long start = System.nanoTime();
a.start();
b.start();
a.join();
b.join();
return (System.nanoTime() - start) / 1_000_000;
}
public static void main(String[] args) throws InterruptedException {
run(0, 1);
run(0, 16);
long sameLine = run(0, 1);
long padded = run(0, 16);
System.out.println("Neighbors (same cache line): " + sameLine + " ms");
System.out.println("Padded (separate lines): " + padded + " ms");
}
}Neighbors (same cache line): 187 ms
Padded (separate lines): 71 msExplanation: Both versions do exactly the same work, and the threads never touch each other's counter. When the counters are neighbors, they share a cache line, so each increment by one core invalidates the other core's cache, and the program ran about two and a half times slower. In one of three runs on the test machine the difference disappeared, probably because the operating system placed both threads on cores that share a cache; this is why such effects must be measured several times. With 128 bytes of space between them, each counter lives on its own line and the cores work independently. (Each version runs once first to warm up the JIT compiler.)
Fixes
| Fix | How |
|---|---|
| Padding | Put unused fields or array slots between hot variables |
@jdk.internal.vm.annotation.Contended | Tells the JVM to pad a field; JDK-internal, works for application code only with -XX:-RestrictContended and an --add-exports flag |
| Per-thread data | Give each thread its own counter and combine at the end |
LongAdder | Already uses padded cells to avoid false sharing |
Common Mistakes
Packing per-thread counters into one small array or object. That's the classic false-sharing layout.
Padding everything. It wastes memory where there's no contention.
Benchmarking without a warm-up. The first runs include JIT compilation time.
Practice
- Easy: Run
FalseSharingseveral times and calculate the average slowdown on your computer. - Harder: Try distances of 2, 4 and 8 array slots between the counters and find the smallest distance that removes the slowdown on your machine.
Key Takeaways
Was this tutorial helpful?