Back to Advanced: Disruptor, Actor, False Sharing
Q2Question 2 of 5

What is false sharing?

Key Concepts
  • False sharing occurs when two threads write to different variables that reside on the same CPU cache line (typically 64 bytes), causing constant cache invalidation between cores.
  • Each write invalidates the entire cache line on other cores, even though the other thread's variable was not modified.
  • Performance degrades 10-100x for what looks like completely independent, non-shared data.
  • Fix with manual padding: insert 7 long fields (56 bytes) between the two hot variables to separate them onto different cache lines.
  • Fix with @jdk.internal.vm.annotation.Contended (Java 8+): the JVM automatically inserts 128 bytes of padding around the annotated field.
  • LongAdder is fast because its internal Cell class uses @Contended to place each cell on its own cache line, eliminating false sharing.
  • False sharing is invisible in standard profilers and lock contention tools; it can only be detected with hardware cache miss counters (perf stat, JMH @AuxCounters).

Definition

False sharing is a performance problem where two threads writing to independent variables on the same CPU cache line cause constant cross-core cache invalidation, degrading throughput by 10-100x even though no data is actually shared.
  • Cache line – the smallest unit of data transfer between CPU cache and main memory, typically 64 bytes on modern CPUs.
  • False sharing – two variables fit on the same 64-byte cache line. Thread-1 writes variable-A on Core-1. The CPU invalidates the entire cache line on Core-2, even though Thread-2 only uses variable-B (which was not modified). Thread-2 must reload the line from L3 cache or main memory. This ping-pong happens on every write.
  • Manual padding – insert dummy fields (7 longs = 56 bytes) between hot variables to push them onto separate cache lines.
  • @Contended – @jdk.internal.vm.annotation.Contended tells the JVM to insert padding automatically (128 bytes by default). Requires -XX:-RestrictContended or internal module access.
  • LongAdder – uses @Contended on its internal Cell class so each thread's cell occupies its own cache line, which is why LongAdder is 10x faster than AtomicLong under high contention.

Two tellers at National Bank of India sit at separate desks with their own calculators, but the calculators are placed on the same small shared shelf. When Rajesh presses a button on his calculator, the entire shelf vibrates, resetting Priya's calculator display. Priya must re-read her number. They are not sharing data, they are sharing the shelf (cache line). The fix is to give each teller their own shelf (padding). No more vibration, no more interference.

Understanding the Concepts

What is false sharing?

False sharing is a hardware-level performance problem. When two threads on different CPU cores write to different variables that happen to reside on the same cache line, each write invalidates the entire cache line on the other core. The other core must reload the line from L3 cache or main memory before it can write its own variable. This back-and-forth invalidation is called cache-line ping-pong.

The performance impact is severe: each write that would normally take 1-5 nanoseconds (L1 hit) takes 40-100 nanoseconds (L3 or main memory access) instead. Under high contention, this means 10-100x slower throughput for operations that appear completely independent.

How does it work at the hardware level?

Modern CPUs use the MESI protocol (Modified, Exclusive, Shared, Invalid) to maintain cache coherence across cores. When Core-1 writes to a cache line, the protocol changes the line's state to Modified on Core-1 and Invalid on all other cores. Any subsequent read or write by Core-2 to any byte on the same line requires Core-2 to fetch the updated line from Core-1's cache.

JAVA
package com.bank.concurrency;

public class FalseSharingDemo {
    // WRONG: both counters on the SAME cache line
    static volatile long buyCounter;   // 8 bytes, offset 16 (after object header)
    static volatile long sellCounter;  // 8 bytes, offset 24
    // Both fit within one 64-byte cache line!
    // Every write to buyCounter invalidates sellCounter on other core

    public static void main(String[] args) throws InterruptedException {
        long start = System.nanoTime();
        Thread buyer = new Thread(() -> {
            for (long i = 0; i < 100_000_000L; i++) buyCounter++;
        }, "BuyCounter");
        Thread seller = new Thread(() -> {
            for (long i = 0; i < 100_000_000L; i++) sellCounter++;
        }, "SellCounter");
        buyer.start();
        seller.start();
        buyer.join();
        seller.join();
        long elapsed = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Time with false sharing: " + elapsed + " ms");
    }
}

How do you fix false sharing with manual padding?

Insert dummy long fields between the two hot variables. Seven longs (7 x 8 = 56 bytes) plus the 8-byte variable itself equals 64 bytes, ensuring each variable occupies its own cache line.

JAVA
package com.bank.concurrency;

public class PaddedCountersDemo {
    // CORRECT: each counter on its own cache line
    static volatile long buyCounter;
    static long p1, p2, p3, p4, p5, p6, p7;  // 56 bytes padding
    static volatile long sellCounter;
    // buyCounter and sellCounter are now on DIFFERENT cache lines
    // No false sharing: writes are independent

    public static void main(String[] args) throws InterruptedException {
        long start = System.nanoTime();
        Thread buyer = new Thread(() -> {
            for (long i = 0; i < 100_000_000L; i++) buyCounter++;
        }, "BuyCounter");
        Thread seller = new Thread(() -> {
            for (long i = 0; i < 100_000_000L; i++) sellCounter++;
        }, "SellCounter");
        buyer.start();
        seller.start();
        buyer.join();
        seller.join();
        long elapsed = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Time without false sharing: " + elapsed + " ms");
    }
}
TEXT
Time with false sharing:    2850 ms
Time without false sharing:  320 ms   (roughly 9x faster)

How does @Contended work?

The @jdk.internal.vm.annotation.Contended annotation (previously @sun.misc.Contended) tells the JVM to insert padding around the annotated field automatically. The default padding is 128 bytes (two cache lines) to account for hardware prefetching of adjacent lines.

JAVA
// @Contended usage (requires --add-opens or -XX:-RestrictContended)
// import jdk.internal.vm.annotation.Contended;
//
// public class ContendedCounters {
//     @Contended volatile long buyCounter;
//     @Contended volatile long sellCounter;
// }
// JVM inserts 128 bytes of padding around each field
// Each field on its own cache line (and its own prefetch line)

How does LongAdder use @Contended internally?

LongAdder achieves its high throughput by striping increments across an array of Cell objects. Each Cell is annotated with @Contended, which ensures it occupies its own cache line. This means threads incrementing different cells do not cause false sharing.

JAVA
// Inside java.util.concurrent.atomic.Striped64:
// @Contended static final class Cell {
//     volatile long value;
//     // ... padding added by @Contended annotation
// }
// Each Cell on its own cache line: no false sharing!
// This is WHY LongAdder is 10x faster than AtomicLong under contention.

How is false sharing different from true sharing?

True sharing occurs when two threads write to the same variable. False sharing occurs when two threads write to different variables that happen to share the same cache line. The symptoms are similar (cache invalidation, performance degradation), but the fix is different.

| Aspect | True sharing | False sharing |
|---|---|---|
| Same variable? | Yes | No (different variables) |
| Data dependency? | Yes | No |
| Visible in code? | Yes (shared field) | No (layout-dependent) |
| Fix | Lock, Atomic, CAS | Padding, @Contended |
| Profiler detection | Lock contention shown | Invisible (only cache counters) |

How do you detect false sharing?

False sharing is invisible in standard profilers and thread contention tools. The only way to detect it is through hardware performance counters that track cache misses and cache-to-cache transfers.

On Linux, use perf stat -e cache-misses,cache-references to compare runs with and without padding. In JMH benchmarks, use @AuxCounters to measure L1 cache miss rates. A high cache miss rate on variables that should be thread-local is a strong indicator of false sharing.

What should a senior engineer know?

Some server CPUs use 128-byte cache lines (prefetching pairs of 64-byte lines), which is why @Contended uses 128 bytes of padding by default. Object layout in Java is controlled by the JVM, and the order of fields in the source code does not necessarily match the order in memory. Use -XX:+PrintFieldLayout or JOL (Java Object Layout) tool to inspect actual field offsets.

What are common interview follow-up questions?

  • Does false sharing affect read-only workloads? No. False sharing only occurs when at least one thread writes, which invalidates the cache line on other cores.
  • Can false sharing occur between two different objects? Yes, if the objects are allocated adjacently on the heap and their hot fields happen to be on the same cache line.

Visual Overview

DIAGRAM
  False Sharing: Two Variables, One Cache Line
  ════════════════════════════════════════════

  Without padding (false sharing):
  ┌──────────────────── Cache Line (64 bytes) ────────────────────┐
  │ [object header 16B] [buyCounter 8B] [sellCounter 8B] [pad]   │
  └──────────────────────────────────────────────────────────────-┘
  Core-1 writes buyCounter → INVALIDATES entire line on Core-2
  Core-2 writes sellCounter → INVALIDATES entire line on Core-1
  Constant ping-pong! 10-100x slower!

  With padding (no false sharing):
  ┌──────────── Cache Line 1 (64 bytes) ─────────────┐
  │ [object header 16B] [buyCounter 8B] [padding 40B] │
  └───────────────────────────────────────────────────┘
  ┌──────────── Cache Line 2 (64 bytes) ─────────────┐
  │ [sellCounter 8B] [padding 56B]                    │
  └───────────────────────────────────────────────────┘
  Core-1 writes buyCounter → Line 2 NOT affected
  Core-2 writes sellCounter → Line 1 NOT affected
  Independent! Full speed!

Key Takeaway

False sharing is a hardware-level performance trap where two threads writing to independent variables on the same 64-byte cache line cause constant cross-core cache invalidation. Each write forces the other core to reload the entire line, turning a 1-5 nanosecond L1 cache hit into a 40-100 nanosecond L3 or main memory access. The result is 10-100x slower throughput for operations that appear completely independent in the source code.

The fix is padding: insert enough dummy bytes between hot variables to ensure each one occupies its own cache line. Manual padding uses 7 long fields (56 bytes) between variables. The @Contended annotation tells the JVM to insert 128 bytes of padding automatically. LongAdder is fast because its internal Cell class uses @Contended, placing each thread's counter on its own cache line.

False sharing is invisible in standard profilers because there is no lock contention to measure. It can only be detected with hardware cache miss counters. At a high-frequency trading desk at National Bank of India, buy and sell counters on adjacent fields ran at 50 million operations per second. After padding, throughput jumped to 480 million, a nearly 10x improvement.

Remember one sentence: false sharing happens when independent variables share a cache line, and the fix is padding them apart so each variable has its own 64-byte line.

---

Share
Use ← → arrow keys to navigate