Fundamentals 25 min read

Understanding Java Biased Locking: Theory, Evolution, and Practical Experiments

This article provides an in‑depth, bottom‑up analysis of Java biased locking, covering its background, lock evolution, detailed mechanisms such as epoch, bulk rebias and revocation, practical JOL experiments, interactions with hashCode and wait, and the recent deprecation in modern JDK releases.

Wukong Talks Architecture
Wukong Talks Architecture
Wukong Talks Architecture
Understanding Java Biased Locking: Theory, Evolution, and Practical Experiments

Introduction

Hello, I am Wukong. This article analyzes biased locking from a low‑level perspective, aiming to give readers fresh insights.

Concurrent Programming Article Collection

Below is a curated list of my previous articles on concurrency programming for easy reference:

1. Discovering the importance of volatile by reading source code

2. Why a programmer’s wife despises him at night – the simplicity of CAS

3. Explaining the ABA problem with building blocks

4. 21 pictures illustrating thread‑unsafe collections

5. 5,000‑word, 24‑image deep dive into Java’s 21 lock types

6. Detailed explanation of 18 queue implementations

7. 16 multithreading interview questions (free download)

More resources are available on my personal website: www.passjava.cn

Background

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

Ordinary synchronized method – locks the current instance.

Static synchronized method – locks the Class object.

Synchronized block – locks the object specified in the parentheses.

Example of a synchronized block

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

After compiling with javap -v , the bytecode contains the instructions:

The monitorenter instruction is inserted at the beginning of the synchronized block, and monitorexit is inserted at the end (including exception paths). Each object has an associated monitor; when a thread reaches monitorenter , it acquires the monitor ownership, i.e., the lock.

If another thread reaches the block while the monitor is owned, it blocks and the OS switches the thread from user mode to kernel mode to perform scheduling, causing a costly context switch. This behavior is called a heavyweight lock and is inefficient.

Lock Evolution

Starting with JDK 1.6, the JVM tries to make locks lighter.

Lightweight Lock – CPU CAS

If the CPU can acquire/release the lock using a simple CAS operation, no context switch occurs, making it much cheaper than a heavyweight lock. However, under heavy contention, CAS retries become wasteful, and the lock may be upgraded to a heavyweight lock.

Biased Lock

Biased locking assumes that a lock is usually acquired by the same thread. The lock object records the thread ID; subsequent acquisitions by the same thread succeed without any CAS, making it even lighter than a lightweight lock.

When multiple threads compete, the biased lock can be upgraded.

Key idea: use as few resources as possible to achieve the highest execution speed.

Biased lock – used when there is no contention and a single thread repeatedly enters the critical section.

Lightweight lock – used when multiple threads can interleave in the critical section.

Heavyweight lock – delegated to the OS mutex when many threads contend simultaneously.

Understanding the Java Object Header

The object header consists of up to three parts:

MarkWord – stores lock state, hash code, GC age, etc.

Class metadata address.

Array length (only for arrays).

The MarkWord is the key field for lock state. It can represent four states: no lock, biased lock, lightweight lock, heavyweight lock. In a 64‑bit JVM the layout is illustrated in the following image (colors omitted for brevity).

Biased Lock in Practice

We can observe the MarkWord changes using the OpenJDK tool JOL (Java Object Layout) . Add the following Maven dependency:

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

or Gradle:

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

Scenario 1 – No startup delay

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

Running the program shows the object starts in an unlocked state and becomes a lightweight lock when the block is entered because the JVM’s biased‑locking startup delay (default ~4 s) has not elapsed.

Setting -XX:BiasedLockingStartupDelay=0 removes the delay, allowing the object to become biased immediately.

Scenario 2 – After the delay

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000); // wait for biased‑locking to become active
  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());
  }
}

Now the object is created in a biasable state and becomes a biased lock when the first thread enters the block.

Scenario 3 – Re‑biasing and revocation

public static void main(String[] args) throws InterruptedException {
  Thread.sleep(5000);
  Object o = new Object();
  log.info("Before sync, MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Main thread acquires lock, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
  Thread t2 = new Thread(() -> {
    synchronized (o) {
      log.info("New thread acquires lock, MarkWord:");
      log.info(ClassLayout.parseInstance(o).toPrintable());
    }
  });
  t2.start();
  t2.join();
  log.info("After new thread, main thread checks MarkWord:");
  log.info(ClassLayout.parseInstance(o).toPrintable());
  synchronized (o) {
    log.info("Main thread re‑enters, MarkWord:");
    log.info(ClassLayout.parseInstance(o).toPrintable());
  }
}

The output shows the lock being biased to the main thread, then upgraded to a lightweight lock when the second thread contends, and finally becoming non‑biasable after the contention.

Bulk Rebias and Bulk Revoke

When a class experiences many biased‑lock revocations, the JVM uses two thresholds:

BiasedLockingBulkRebiasThreshold = 20 – after 20 revocations, the JVM performs a bulk rebias, incrementing the class’s epoch and updating all currently biased objects.

BiasedLockingBulkRevokeThreshold = 40 – after 40 revocations, the class is marked as non‑biasable and all future locks become lightweight.

Between these thresholds, a decay timer ( BiasedLockingDecayTime = 25000 ms) gives the class a chance to reset the counter if no further contention occurs.

Interaction with hashCode

If Object.hashCode() or System.identityHashCode() is invoked before a lock is biased, the object’s MarkWord stores the hash value, causing the JVM to fall back to a lightweight lock when the object is later synchronized.

When the object is already biased and hashCode() is called, the lock is upgraded directly to a heavyweight lock.

Effect of Object.wait()

Calling wait() inside a synchronized block always upgrades the lock to a heavyweight (OS mutex) lock because the thread must release the monitor and be parked, which requires OS‑level synchronization.

Deprecation of Biased Locking

JEP 374 deprecates and disables biased locking starting with JDK 15. The feature can still be enabled explicitly with the JVM flag -XX:+UseBiasedLocking , but the default is now disabled because the maintenance cost outweighs the performance benefit for most workloads.

Summary

Biased locking is still useful for Java 8‑14 environments and for interview questions.

Understanding its lifecycle helps you reason about lock upgrades, hashCode interactions, and wait semantics.

When the cost of maintaining biased locking outweighs its benefits, the JVM disables it – a practical illustration of the Occam’s razor principle in software design.

Key Source References

Biased locking entry: bytecodeInterpreter.cpp#l1816

Biased revocation entry: interpreterRuntime.cpp#l608

Biased unlock entry: bytecodeInterpreter.cpp#l1923

Further Reading

Oracle biased locking presentation (OOPSLA 2006).

OpenJDK HotSpot synchronization documentation.

Various community articles and blog posts linked throughout the text.

JavaJVMconcurrencySynchronizationlock optimizationBiased Locking
Wukong Talks Architecture
Written by

Wukong Talks Architecture

Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.

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.