Why synchronized Got Fast: The Rise and Removal of Biased Locking
For years, Java developers heard the same advice: “synchronized is slow, so avoid it.” Then, around Java 6, that advice quietly stopped being true. A hidden HotSpot optimization called biased locking was a big reason why.
Later, the same optimization was switched off and finally deleted. In this article, we follow that story, confirm the exact JEP number, measure what the change means on real JVMs, and see how to write code that no longer depends on it.
The Problem Biased Locking Solved
Most locks in real programs are never fought over. A single thread locks an object again and again, and nobody else ever touches it. Yet taking even an uncontended lock needs an atomic compare-and-swap operation, which was expensive on older hardware. A 2006 slide deck on Java SE 6 synchronization by HotSpot engineer Dave Dice lists the high latency of atomic CAS as a key uncontended cost.
The Rise: Locks That Remember Their Owner
How it works
The idea is simple. According to JEP 374, the first lock of a monitor biases it towards that thread, and later synchronized operations by the same thread skip the atomic instruction. Put differently, the JVM bets that the same thread will come back.
Oracle’s Java SE 6 performance white paper states that biased locking is enabled by default in Java SE 6 and later. It builds on the Russell and Detlefs OOPSLA 2006 paper. In addition, the OpenJDK HotSpot wiki notes that it activated only some seconds after startup.
The price of losing the bet
However, the bet could fail. When a second thread wants a biased lock, the JVM must revoke the bias, and JEP 374 calls that revocation expensive. Consequently, Oracle’s JRockit to HotSpot migration guide advises that highly contended applications may do better with the option disabled.
The Fall: JEP 374 and Removal
Confirming the JEP number
The JEP you are looking for is JEP 374, titled “Deprecate and Disable Biased Locking”, delivered in JDK 15. It disabled biased locking by default and deprecated its options. Importantly, the final removal was not a separate JEP. It was a regular change, tracked as JDK-8256425, which obsoleted the code in JDK 18.
| Release | What happened | Reference |
|---|---|---|
| Java SE 6 | Enabled by default | Oracle white paper |
| JDK 15 | Disabled by default, options deprecated | JEP 374 |
| JDK 18 | Code removed, flags obsolete | JDK-8256425 |
| JDK 19 | Flag scheduled to expire | hotspot-dev review thread |
Why it had to go
The linked implementation ticket gives three reasons. First, atomic instructions became cheaper on modern processors. Second, many programs that benefited were older ones using classes such as Hashtable and Vector, which synchronize on every call. Third, biased locking added complex code that reached into other parts of HotSpot.
What the Numbers Say
To see the effect, I installed JDK 11, 17, 21 and 25 and ran two micro-benchmarks on a single thread. One increments a counter inside a synchronized method. The other appends a character to a StringBuffer, whose methods are all synchronized. On JDK 11 and 17, I ran with biased locking on and off.
| JVM setting | Counter (ns per op) | StringBuffer (ns per op) |
|---|---|---|
| JDK 11, biased locking on (default) | 0.29 | 1.7 |
| JDK 11, biased locking off | 5.5 | 22.2 |
| JDK 17, biased locking on (flag) | 0.24 | 1.9 |
| JDK 17, default (off) | 5.4 | 21.9 |
| JDK 21 | 5.4 | 22.0 |
| JDK 25 | 5.4 | 21.4 |


Clearly, biased locking made an uncontended lock about twenty times cheaper in this best-case test. Moreover, with it enabled, the synchronized StringBuffer ran about as fast as the unsynchronized StringBuilder. Once it was off, the same code was roughly thirteen times slower than StringBuilder.
Still, keep the scale in mind. Five nanoseconds per lock means one million uncontended lock operations cost about five milliseconds. Also, JDK 21 refused the old flag outright as an unrecognized option, which confirms it is gone. A ticket linked to the removal, JDK-8329619, does report a roughly fourfold regression in a Spark compression micro-benchmark without it, so some workloads clearly felt the change.
What It Means for Your Code
The lesson is that “synchronized is fast” was partly a JVM trick on top of uncontended code. Now that the trick is gone, the honest fix is to stop paying for locks you do not need. JEP 374 itself says applications that relied on unnecessary synchronization will likely improve after switching to newer, unsynchronized classes.
| Legacy synchronized class | Modern replacement |
|---|---|
| StringBuffer | StringBuilder |
| Vector | ArrayList |
| Hashtable | HashMap, or ConcurrentHashMap if shared |
Quick checklist
- Remember it is JEP 374 (JDK 15), and the removal was JDK 18.
- Delete any biased locking flags from your startup scripts.
- Replace StringBuffer, Vector and Hashtable where one thread owns them.
- Measure after upgrading from JDK 11, especially for legacy code.
- Use real concurrent collections when threads truly share data.
What We Have Learned
Biased locking let the first thread to lock an object skip atomic instructions, and it was on by default from Java SE 6. Over time, hardware made atomics cheaper, the biggest winners were legacy synchronized classes, and the code was costly to maintain. Therefore, JEP 374 disabled it in JDK 15 and JDK 18 removed it. In our measurements, an uncontended lock cost about 0.3 ns with biased locking and about 5.4 ns without it, which is small but visible in legacy-heavy code. So, the best response is simple: use unsynchronized classes when a single thread owns the data.

