Fundamentals 26 min read

Why Java’s Biased Locking Matters—and Why It’s Being Deprecated

This article explains the evolution of Java synchronization mechanisms from the classic synchronized keyword to lightweight, biased, and heavyweight locks, explores how lock states transition, the impact of hashCode and wait, and why biased locking is being phased out in modern JDKs.

Programmer DD
Programmer DD
Programmer DD
Why Java’s Biased Locking Matters—and Why It’s Being Deprecated

Background

Before JDK 1.5, the synchronized keyword was the primary solution for Java concurrency:

Ordinary synchronized method locks the current instance object.

Static synchronized method locks the current class Class object.

Synchronized block locks the object specified in the parentheses.

Example using a synchronized block:

public void test() { synchronized (object) { i++; } }

After compiling with javap -v, the generated bytecode includes monitorenter at the start of the synchronized block and monitorexit at the end (including exception paths). Each object has a monitor associated with it; when a thread reaches monitorenter, it acquires the monitor’s ownership, i.e., the lock.

If another thread reaches the synchronized block while the monitor is owned, it blocks and the control switches from user mode to kernel mode, causing a context switch that incurs significant overhead. This heavy-weight behavior gave synchronized a reputation for poor performance.

Evolution of Locks

In JDK 1.6, the JVM introduced optimizations to make locks lighter:

Lightweight Lock: CPU CAS

If the CPU can handle lock acquisition/release with a simple CAS operation, no context switch is needed, making it much lighter than a heavyweight lock. However, under high contention, CAS attempts become wasteful, and the lock may be upgraded to a heavyweight lock.

HotSpot’s authors observed that most of the time there is no thread competition and the same thread repeatedly acquires the lock, so they sought a cheaper way.

Biased Lock

Biased locking assumes a lock is “biased” toward a single thread; the lock records the thread ID and can be acquired without CAS if the same thread re-enters. This is a load-and-test process, slightly lighter than CAS.

When multiple threads compete, the biased lock upgrades to a lightweight lock.

"The fewer resources used, the faster the program runs."

Summary of lock types:

Biased lock: used when there is no competition and a single thread enters the critical section.

Lightweight lock: multiple threads can alternate in the critical section.

Heavyweight lock: many threads compete, delegating to the OS mutex.

Key questions remain:

Where is the thread ID stored in the lock object?

How does the upgrade process transition?

Understanding Java Object Header

Instead of maintaining a separate mapping for thread IDs, the JVM stores lock information directly in the object header. The object header consists of three parts: MarkWord Class metadata address

Array length (only for arrays)

The MarkWord holds the lock state and can transition among four states (no lock, biased, lightweight, heavyweight) using bits stored in the 64‑bit word.

Understanding how MarkWord changes is essential for the following sections.

Understanding Biased Lock

The OpenJDK JOL (Java Object Layout) tool can display the object layout. Add the following dependency to your project:

Maven

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.14</version>
</dependency>

Gradle

implementation 'org.openjdk.jol:jol-core:0.14'

Using JOL, we can observe lock states in three scenarios.

Scenario 1

public static void main(String[] args) {
  Object o = new Object();
  log.info("Before synchronized, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Inside synchronized, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}

Output (JOL 0.14):

With JOL 0.16 the output is more readable:

Although biased locking is enabled by default, the object starts in an unlocked state because the JVM delays biased locking (≈4 s) to avoid unnecessary upgrades.

"Even though biased locking is enabled, the JVM delays activation for a few seconds to reduce the cost of premature upgrades."

You can set -XX:BiasedLockingStartupDelay=0 to remove the delay, but it is not recommended.

Scenario 2

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  log.info("Before synchronized, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Inside synchronized, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}

Output shows the object entering biased state after the 5 s pause.

Scenario 3

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  synchronized (o) {
    log.info("Main thread inside, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
  Thread t2 = new Thread(() -> {
    synchronized (o) {
      log.info("New thread inside, MarkWord:");
      log.info(ClassLayout.parseInstance(o).toPrintable());
    }
  });
  t2.start();
  t2.join();
  log.info("Main thread after new thread, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Main thread again, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}

Result (annotations): Mark 1: initial biasable state. Mark 2: biased to main thread after first block. Mark 3: new thread enters, lock upgrades to lightweight. Mark 4: new thread exits, main thread sees non‑biasable state. Mark 5: main thread re‑enters, uses lightweight lock.

These observations lead to the concept of biased‑lock revocation and the need for bulk‑rebias and bulk‑revoke mechanisms.

Biased Revocation

Revocation occurs when multiple threads compete for a lock that was previously biased. It differs from lock release, which simply exits the synchronized block.

Revocation: the lock can no longer stay biased because of contention.

Release: normal exit from a synchronized method or block.

Revocation requires reaching a safepoint so that the thread holding the biased lock can be paused.

"The revocation flips the third bit of the MarkWord from 1 to 0."

If the owning thread is dead or has exited the block, revocation is straightforward; otherwise the lock upgrades to lightweight.

Bulk Rebias (Bulk Rebias)

Each class maintains a counter of biased‑lock revocations. When the counter reaches BiasedLockingBulkRebiasThreshold = 20, the JVM performs a bulk rebias, updating the epoch field of all currently biased objects. BiasedLockingBulkRebiasThreshold = 20 The epoch acts like a timestamp; when it changes, biased objects compare their own epoch with the class epoch to decide whether to keep the bias.

Bulk Revoke (Bulk Revoke)

If the revocation counter reaches BiasedLockingBulkRevokeThreshold = 40, the class is marked non‑biasable and all future locks use the lightweight path. BiasedLockingBulkRevokeThreshold = 40 A decay timer ( BiasedLockingDecayTime = 25000 ms) gives the JVM a chance to reset the counter if no further revocations occur within 25 seconds.

BiasedLockingDecayTime = 25000

Where Did HashCode Go?

HashCode is stored in the object header only after the first call to Object::hashCode() or System::identityHashCode(). When a biased object generates a hashcode, the lock upgrades to a heavyweight lock.

Scenario 1

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  log.info("Before hashcode, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  o.hashCode();
  log.info("After hashcode, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Inside synchronized, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}
Even if an object is initially biasable, calling hashCode() forces it to use a lightweight lock.

Scenario 2

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  synchronized (o) {
    log.info("Inside first block, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
  o.hashCode();
  log.info("After hashcode, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Same thread again, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}
After generating a hashcode, the lock becomes lightweight.

Scenario 3

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  synchronized (o) {
    log.info("Inside block, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
    o.hashCode();
    log.info("After hashcode in biased state, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}
Generating a hashcode while the object is biased upgrades it directly to a heavyweight lock.

What Happens When Object.wait() Is Called?

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  synchronized (o) {
    log.info("Inside block, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
    o.wait(2000);
    log.info("After wait, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}
Calling wait() forces the lock to become a heavyweight (OS mutex) lock.

Farewell to Biased Lock

JEP 374 deprecates biased locking because its maintenance cost outweighs its benefits. Starting with JDK 15, biased locking is disabled by default unless explicitly enabled with -XX:+UseBiasedLocking.

The added complexity makes it hard for most developers to understand and maintain, prompting its removal.

Conclusion

Java 8‑based projects can still use biased locking safely.

Interviewers frequently ask about lock optimizations.

Future JVM versions may re‑introduce improved biasing mechanisms.

Apply Occam’s razor: avoid adding complexity unless it provides clear benefits.

Key HotSpot source entries for further exploration:

Biased lock entry: bytecodeInterpreter.cpp

Biased revocation entry: interpreterRuntime.cpp

Biased lock release entry: bytecodeInterpreter.cpp

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMconcurrencySynchronizationlock optimizationBiased Locking
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.