Keeping in mind the world of multi-threaded programming, optimizing performance is often a never-ending task. One of the bottlenecks that is often neglected by developers is false sharing. This article deep dives to understand what false sharing is, how it impacts performance, and other ways to mitigate it using practical examples in Java.
🍥What is False Sharing?
False sharing occurs when multiple threads modify variables that reside on the same cache line. A cache line is the smallest unit of data that can be transferred between the main memory (RAM) and the CPU cache. Modern CPUs cache data in chunks (typically 64 bytes), and when one thread updates a variable in a cache line, it invalidates the entire cache line for other threads. This forces other threads to reload the cache line from memory, even if they are accessing different variables within the same cache line.
The result? Unnecessary cache invalidations and reloads, leading to significant performance degradation, especially in high-concurrency scenarios.
🚨The Problem: False Sharing in Action
Let’s start by looking at a simple example that demonstrates false sharing. Consider the following Java code:
public class FalseSharingProblem {
public static void main(String[] args) {
FalseSharingCounter falseSharingCounter1 = new FalseSharingCounter();
FalseSharingCounter falseSharingCounter2 = falseSharingCounter1;
Runnable r1 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter1.count1++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Runnable r2 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter2.count2++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Thread.ofPlatform().name("Thread1").start(r1);
Thread.ofPlatform().name("Thread1").start(r2);
}
}
public class FalseSharingCounter {
public volatile int count1 = 0;
public volatile int count2 = 0;
}
In this example, two threads (Thread1 and Thread2) are incrementing two different counters (count1 and count2) that reside in the same FalseSharingCounter object. Since count1 and count2 are likely to be on the same cache line, updating one counter will invalidate the cache line for the other thread, causing false sharing.
🔍The Impact
When you run this code, you’ll notice that the time taken to complete the increments is significantly higher than expected. This is due to the constant cache line invalidations caused by false sharing.
✨The Artificial Solution: Separate Objects
One way to mitigate false sharing is to ensure that the counters are not on the same cache line. This can be achieved by using separate objects for each counter:
public class FalseSharingArtificialSolution {
public static void main(String[] args) {
FalseSharingCounter falseSharingCounter1 = new FalseSharingCounter();
FalseSharingCounter falseSharingCounter2 = new FalseSharingCounter();
Runnable r1 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter1.count1++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Runnable r2 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter2.count2++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Thread.ofPlatform().name("Thread1").start(r1);
Thread.ofPlatform().name("Thread1").start(r2);
}
}
public class FalseSharingCounter {
public volatile int count1 = 0;
public volatile int count2 = 0;
}
In this solution, falseSharingCounter1 and falseSharingCounter2 are two separate objects, ensuring that count1 and count2 are not on the same cache line. This eliminates false sharing, and you'll observe a significant improvement in performance.
đź§°The Elegant Solution: Using @Contended
While the artificial solution works, it’s not always practical to create separate objects for every counter. One of the solution is to add padding to the variables. We can do manually but Java provides a more elegant solution using the @jdk.internal.vm.annotation.Contended annotation. This annotation tells the JVM to add padding around the annotated field or class to prevent false sharing.
Example 1: Padding a Single Field
public class FalseSharingContendedCounter1 {
// this mean this jvm will pad it so that it will not be in same cache line as other fields of this class
@jdk.internal.vm.annotation.Contended
public volatile int count1 = 0;
public volatile int count2 = 0;
}
In this example, count1 is padded to ensure it doesn't share a cache line with count2.
Example 2: Padding the Entire Class
@jdk.internal.vm.annotation.Contended
public class FalseSharingContendedCounter2 {
public volatile int count1 = 0;
public volatile int count2 = 0;
}
Here, the entire class is padded, ensuring that none of its fields share a cache line.
Example 3: Grouping Fields
public class FalseSharingContendedCounter3 {
@jdk.internal.vm.annotation.Contended("group1")
public volatile int count1 = 0;
@jdk.internal.vm.annotation.Contended("group1")
public volatile int count2 = 0;
@jdk.internal.vm.annotation.Contended("group2")
public volatile int count3 = 0;
}
In this example, count1 and count2 are grouped together and will share the same cache line, while count3 is placed in a different cache line.
Running the Contended Solution
To use the @Contended annotation, you need to run your Java program with the -XX:-RestrictContended JVM option:
Here’s the complete code for the contended solution:
// use -XX:-RestrictContended cm options to run this
public class FalseSharingContendedSolution {
public static void main(String[] args) {
FalseSharingContendedCounter1 falseSharingCounter1 = new FalseSharingContendedCounter1();
FalseSharingContendedCounter1 falseSharingCounter2 = falseSharingCounter1;
Runnable r1 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter1.count1++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Runnable r2 = () -> {
int iterations = 1_000_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
falseSharingCounter2.count2++;
}
System.out.println("Time taken "+(System.currentTimeMillis()-start)+" ms");
};
Thread.ofPlatform().name("Thread1").start(r1);
Thread.ofPlatform().name("Thread1").start(r2);
}
}
✍️Conclusion
False sharing is a subtle but significant performance issue in multi-threaded applications. By understanding how cache lines work and using techniques like object separation or the @Contended annotation, you can mitigate false sharing and improve the performance of your Java applications.
Remember, in the world of high-performance computing, every nanosecond counts. So, the next time you’re dealing with multi-threaded counters or shared variables, don’t forget to check for false sharing!
🎗️Reference
False Sharing in Java — Jakob Jenkov
Happy coding! 🚀

Top comments (0)