A Comprehensive Guide to Java Concurrency and Synchronization Mechanisms
This article provides an in-depth exploration of Java concurrency and synchronization mechanisms, detailing the evolution of the Java Memory Model, explaining core concepts like volatile, CAS, synchronized, and park/unpark, and illustrating their practical applications through code examples and JVM optimization insights.
The article begins by tracing the evolution of Java concurrency, highlighting how hardware advancements from single-core to multi-core processors necessitated robust synchronization mechanisms. Since JDK 1.0, Java has supported multithreading, with major milestones in JDK 1.5 introducing the formal Java Memory Model and the java.util.concurrent package, largely driven by Doug Lea, and JDK 7 adding the fork-join framework.
Concurrency allows multiple threads to execute seemingly simultaneously, but shared resources can lead to atomicity and ordering violations. The article demonstrates this through classic examples like the non-atomic count++ operation and unsafe singleton instantiation:
public class ThreadCount extends Thread {
public static int count = 0;
public static void inc() {
try { Thread.sleep(1); } catch (InterruptedException e) {}
count++;
}
// ... main method spawning 1000 threads ...
}Without synchronization, compiler optimizations such as loop hoisting can cause liveness failures, where one thread never observes another thread's updates to a shared variable. The volatile keyword serves as a lightweight synchronization tool that prevents caching and enforces memory visibility. It establishes happens-before relationships and inserts memory barriers to strictly prohibit instruction reordering around volatile reads and writes.
Compare-And-Swap (CAS) is a hardware-supported atomic instruction used for lock-free synchronization. It compares a memory location's current value with an expected value and updates it only if they match. While efficient, CAS can suffer from the ABA problem and high CPU overhead during prolonged spinning. It forms the foundation for atomic classes:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}The synchronized keyword provides built-in, reentrant mutual exclusion with an optimized lock-upgrading process. In double-checked locking patterns, combining synchronized with volatile is essential to prevent instruction reordering during object initialization:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}Park and unpark are low-level OS-dependent primitives for blocking and waking threads. They underpin higher-level concurrency utilities such as ReentrantLock, Semaphore, CountDownLatch, and CyclicBarrier, enabling complex thread coordination. The article concludes by noting the evolution of thread-safe collections from coarse-grained synchronized containers to fine-grained or lock-free structures in java.util.concurrent.
New Oriental Technology
Practical internet development experience, tech sharing, knowledge consolidation, and forward-thinking insights.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.