Understanding Java Locks, synchronized, ReentrantLock, and Kotlin Coroutine Synchronization
This article explains Java's lock mechanisms—including synchronized, ReentrantLock, and their JVM implementations—covers lock classifications, memory barriers, CAS, and compares them with Kotlin coroutine synchronization tools like Mutex, providing code examples and practical guidance for safe concurrent programming.
Java Lock Classification
Locks can be classified by sharing granularity, fairness, and implementation mechanism.
Exclusive lock : only one thread can hold it at a time (e.g., synchronized , ReentrantLock ).
Shared lock : multiple threads can hold it simultaneously (e.g., read lock of ReentrantReadWriteLock ).
Fair lock : threads acquire locks in request order.
Non‑fair lock : threads may try to barge before queuing.
Lock Implementation Mechanism in the JVM
The synchronized keyword relies on the object header’s Mark Word and a Monitor. Since JDK 1.6 the JVM introduces three lock states that can be upgraded but not downgraded: biased lock, lightweight lock, and heavyweight lock.
Biased lock : optimized for no contention; the thread ID is stored in the Mark Word. When another thread competes, it upgrades to a lightweight lock.
Lightweight lock : uses CAS to replace the Mark Word with a pointer to a lock record in the thread stack; if CAS fails, the thread spins.
Heavyweight lock : falls back to an OS mutex (Monitor), causing thread suspension and context switches.
Typical lock state transitions are illustrated in the table below.
Lock State
Mark Word Content
Flag Bits
Thread ID (optional)
Unlocked
Object hash / GC info
01
None
Biased
Thread ID + timestamp
01
Present
Lightweight
Pointer to lock record
00
Present
Heavyweight
Pointer to Monitor
10
None
synchronized Details
synchronized is a built‑in JVM lock. When a thread enters a synchronized block or method, the JVM inserts monitorenter and monitorexit bytecode instructions; for synchronized methods the flag ACC_SYNCHRONIZED is set.
<code>package com;
public class Test {
int a = 0;
public synchronized void aaa(){
System.out.println("Inside synchronized block");
a = 1;
}
public void bbb(){
synchronized (this){
a = 2;
}
}
}
</code>Disassembling the class shows the monitor instructions:
<code>Compiled from "Test.java"
public class com.Test {
int a;
public com.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.<init>:()V
4: aload_0
5: iconst_0
6: putfield #2 // Field a:I
9: return
public synchronized void aaa();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Inside synchronized block
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: iconst_1
10: putfield #2 // Field a:I
13: return
public void bbb();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: iconst_2
6: putfield #2 // Field a:I
9: aload_1
10: monitorexit
11: goto 19
14: astore_2
15: aload_1
16: monitorexit
17: aload_2
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
}
</code>ReentrantLock Details
ReentrantLock is a flexible lock from java.util.concurrent.locks . It supports reentrancy, fairness, interruptibility, timeout acquisition, and explicit lock/unlock.
Implemented on top of AbstractQueuedSynchronizer (AQS) .
Two internal classes: NonfairSync and FairSync .
<code>static final class NonfairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
</code>The fair version checks hasQueuedPredecessors() before acquiring.
<code>static final class FairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
</code>Comparison: synchronized vs. ReentrantLock
Feature
synchronized ReentrantLockReentrancy
Yes
Yes
Implementation
JVM monitor (object header & Monitor)
AQS (AbstractQueuedSynchronizer)
Lock release
Implicit (method/block exit)
Explicit
unlock()Fairness
Not supported (default non‑fair)
Supports fair and non‑fair modes
Interruptible
No
Yes (lockInterruptibly, tryLock with timeout)
Timeout acquisition
No
Yes (
tryLock(timeout))
Kotlin Coroutine Lock Implementations
Kotlin runs on the JVM and inherits Java's lock mechanisms, but also provides coroutine‑friendly primitives.
Mutex : a lightweight, non‑blocking lock that suspends the coroutine instead of blocking a thread.
Channel : a lock‑free queue for safe data transfer between coroutines.
Actor : encapsulates state in a single‑threaded coroutine, avoiding explicit locks.
Typical Mutex API:
<code>private val mutex = Mutex()
private var count = 0
suspend fun addCount() {
mutex.withLock { // critical section
count++
}
}
</code>Mutex supports lock() , unlock() , tryLock() , and withLock() . It is cancellable: if a coroutine is cancelled while waiting, it is removed from the queue.
Memory Barriers
Java Memory Model (JMM) inserts memory barriers for volatile , synchronized , etc. The main barrier types are LoadLoad, LoadStore, StoreStore, and StoreLoad. For synchronized , entering a monitor performs LoadLoad+LoadStore, and exiting performs StoreStore+StoreLoad.
CAS (Compare‑And‑Swap)
CAS is implemented via sun.misc.Unsafe and native code. It provides a non‑blocking optimistic lock used by lightweight locks, Atomic* classes, and AQS.
Thread States
When a thread cannot acquire a resource it may enter a blocked state; calling wait() , join() , or park() puts it into a waiting state until another thread wakes it.
Conclusion
synchronized and ReentrantLock can guarantee data safety in coroutine contexts, but they block the underlying thread, which harms coroutine concurrency.
For coroutine‑heavy workloads, prefer coroutine‑specific primitives such as Mutex , Channel , or Actor .
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.