Backend Development 22 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Understanding Java Locks, synchronized, ReentrantLock, and Kotlin Coroutine Synchronization

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
ReentrantLock

Reentrancy

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 .

JavaJVMconcurrencyKotlinLocksCoroutines
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.