How Java’s synchronized and CAS Translate into Bytecode: A Deep Dive
This article explores the JVM bytecode instructions behind Java’s synchronized keyword and CAS mechanism, detailing monitorenter/monitorexit, ACC_SYNCHRONIZED flags, static synchronization, and the underlying Unsafe.compareAndSetInt implementation with example code and analysis.
In Java, bytecode instructions are the foundation for JVM execution. This article explains how the synchronized keyword and CAS (Compare‑And‑Swap) mechanism are implemented at the bytecode level.
Bytecode Implementation of synchronized
The synchronized keyword relies on the JVM’s monitor lock and the object header. Depending on the usage, the JVM inserts two kinds of bytecode instructions.
1. Bytecode for synchronized blocks
When synchronized is applied to a code block, the JVM inserts monitorenter and monitorexit instructions to acquire and release the lock.
Example code:
public class SyncDemo {
private final Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
System.out.println("Sync Block");
}
}
}Decompiled with javap -c:
public void syncBlock();
Code:
0: aload_0
1: getfield #2 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // try to acquire lock
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #4 // String Sync Block
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit // release lock
17: goto 25
20: aload_1
21: monitorexit // release on exception
22: aload_2
23: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 22 20 anyKey instruction analysis: monitorenter: attempts to acquire the monitor of the lock object; on success the monitor counter increments. monitorexit: releases the monitor; when the counter reaches zero, waiting threads are awakened.
Exception handling: the JVM automatically inserts a second monitorexit to ensure the lock is released even if an exception occurs.
2. Bytecode for synchronized methods
When synchronized is used on a method, the JVM marks the method with the ACC_SYNCHRONIZED flag. The lock object is this for instance methods.
Example code:
public class SyncDemo {
public synchronized void syncMethod() {
System.out.println("Sync Method");
}
}Decompiled bytecode:
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Sync Method
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: returnKey flag analysis: ACC_SYNCHRONIZED: the JVM automatically acquires the monitor of the current instance ( this) before method execution and releases it afterward.
3. Bytecode for static synchronized methods
Static methods lock on the Class object. The ACC_STATIC flag indicates a static method.
Example code:
public class SyncDemo {
public static synchronized void syncStatic() {
System.out.println("Sync Static");
}
}Decompiled bytecode:
public static synchronized void syncStatic();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Sync Static
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: returnKey flag analysis: ACC_STATIC: indicates a static method; the lock object is SyncDemo.class. ACC_SYNCHRONIZED: JVM manages lock acquisition and release just like for instance methods.
Bytecode Implementation of CAS
CAS (Compare‑And‑Swap) provides lock‑free concurrency. It relies on JNI calls and hardware instructions such as cmpxchgl. Java’s atomic classes (e.g., AtomicInteger) wrap CAS operations.
1. CAS operation bytecode
Using AtomicInteger.compareAndSet as an example, the underlying call is Unsafe.compareAndSetInt, which eventually invokes native C++ code via JNI.
Example code:
public class CASDemo {
private AtomicInteger atomicInt = new AtomicInteger(0);
public boolean compareAndSet(int expected, int newValue) {
return atomicInt.compareAndSet(expected, newValue);
}
}Decompiled bytecode:
public boolean compareAndSet(int, int);
descriptor: (II)Z
flags: ACC_PUBLIC
Code:
0: aload_0
1: getfield #2 // Field atomicInt:Ljava/util/concurrent/atomic/AtomicInteger;
4: iload_1
5: iload_2
6: invokevirtual #3 // Method java/util/concurrent/atomic/AtomicInteger.compareAndSet:(II)Z
9: ireturnKey instruction analysis: invokevirtual: calls AtomicInteger.compareAndSet, which internally uses Unsafe.compareAndSetInt via JNI.
2. Unsafe.compareAndSetInt implementation
The native method Unsafe.compareAndSetInt invokes C++ code that uses the hardware cmpxchgl instruction to perform an atomic compare‑and‑swap.
C++ pseudo‑code (OpenJDK source):
bool compareAndSetInt(JNIEnv* env, jobject obj, jlong offset, jint expected, jint newValue) {
// Obtain the address of the object's field
volatile jint* address = (volatile jint*)((char*)obj + offset);
// Use the cmpxchgl instruction to compare and swap
return Atomic::compare_and_swap(newValue, address, expected);
}Hardware instruction analysis: cmpxchgl (x86): compares the value in a register with the memory location; if equal, it writes the new value, otherwise it loads the memory value into the register.
Bytecode Correlation with Lock Upgrade Mechanism
The JVM records lock state in the object header’s Mark Word. At runtime the lock can be upgraded from no‑lock → biased lock → lightweight lock → heavyweight lock. Although bytecode does not directly show the upgrade, the following aspects are related.
1. Mark Word in the object header
No‑lock state: stores the object’s hash code.
Biased lock state: stores thread ID and bias timestamp.
Lightweight lock state: stores a pointer to a LockRecord in the thread’s stack.
Heavyweight lock state: stores a pointer to a Monitor object.
2. Triggers for lock upgrade
No‑lock → biased lock: the first thread entering a synchronized block uses CAS to set the biased lock flag.
Biased lock → lightweight lock: a second thread attempts to acquire the lock, causing the biased lock to be revoked and the JVM to try a lightweight lock via spinning and CAS.
Lightweight lock → heavyweight lock: if spinning fails, the JVM escalates to a heavyweight lock via a system call.
Summary
synchronized
Code blocks: implemented with monitorenter and monitorexit, relying on the monitor mechanism.
Methods: implemented with the ACC_SYNCHRONIZED flag; the lock object is this for instance methods or the Class object for static methods.
CAS
Atomic operation: performed via Unsafe.compareAndSetInt, which uses JNI and hardware instructions.
Performance advantage: avoids thread blocking and reduces context‑switch overhead.
Lock Upgrade
Dynamic optimization: the JVM adjusts the lock type based on contention, progressing from no‑lock to heavyweight lock.
Object header ( Mark Word) records the lock state and is the core data structure for lock upgrades.
Understanding bytecode instructions and the JVM’s internal mechanisms enables developers to write more efficient concurrent code and optimize performance bottlenecks.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
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.
