DEV Community

Arash Ariani
Arash Ariani

Posted on

Understanding JVM Lock Optimizations

Concurrency is very critical to the development of robust, scalable applications that can perform several concurrent operations. However, a price needs to be paid for this in terms of synchronization. It incurs performance costs due to the attendant overheads of acquiring and releasing locks. To alleviate these performance costs, several optimizations have been incorporated into the JVM, in several flavors, such as biased locking, lock elimination, lock coarsening, and the notion of lightweight and heavyweight locks.

In this article, we see these optimizations in greater detail, going over how they improve synchronization in multi-threaded Java applications.

Java Locking Basics

In Java, the synchronization of blocks or methods ensures that only one thread can execute a critical section of code at a time. This matters in particular when considering the sharing of resources within the multithreaded environment. Java implements this by relying on intrinsic locks- or sometimes, they are called monitors associated with objects or classes that help manage access to the threads by using the synchronized blocks.

Although synchronization is a necessity for thread safety, it might be quite expensive when contention is low or completely absent. Which is where JVM optimizations step into the frame. Thus, that reduces the cost of locking and will enhance overall performance.

1. Biased Locking

What is Biased Locking?

Biased locking is an optimization targeted at the reduction of the overhead of lock acquisition. It is optimized to reduce the cost of lock acquisition, which is dominated by a single thread or most largely accessed by a single thread. Such programs often acquire and release locks by the same thread with no contention from other threads. The JVM can recognize this pattern and biases the lock to that particular thread; following lock acquisition comes almost for free.

How Does Biased Locking Work?

If biased locking is enabled, then the first time a thread acquires a lock, it makes that lock biased toward that thread. The identity of the thread is recorded in the header of the lock object, and subsequent lock acquisitions by that thread involve no synchronization whatsoever-they just check if the lock is biased toward the current thread, which is a very fast, non-blocking operation.

If another thread tries to acquire the lock, then bias is canceled and JVM falls back to a standard unbiased locking mechanism. At this stage, it is now a standard lock, and the second thread will have to acquire it through a standard locking process.
Benefits of Biased Locking

Performance: Acquisition of the same thread on a biased lock is almost a free lock acquisition.

Hence, contention handling is not needed because other threads have no chance to be involved in acquiring the lock.

Lower Overhead: The state of the lock need not change or synchronization-related metadata be modified except in the event of contention. 
 

When is Biased Locking Used?

Biased locking is useful in applications where locks are mainly accessed by the same thread, such as single-threaded applications or an application that has low lock contention under multi-threading. It's enabled by default in most JVMs.

How to Disable Biased Locking

Biased locking is enabled by default but can also be disabled with the JVM flag like below:

-XX:-UseBiasedLocking

2. Lock Elimination

What is Lock Elimination?

Lock elimination is a very powerful optimization in which the JVM eliminates some unnecessary synchronization (locks) completely. It will inspect the code for any opportunities during its JIT compilation wherein it finds out that synchronization is not necessary. This usually occurs when the lock has been accessed by just one thread, or the object that the JVM will be used to synchronize doesn't share the same object within different threads. Once the JVM deems that it is no longer required, then it does eliminate the lock.

How Does Lock Elimination Work?

In the escape analysis phase of JIT compilation, JVM checks whether the object is confined to a single thread or is used only in a local context. If synchronization on that object can be removed because an object doesn't escape the scope of the thread that created it, then it will be so.

For example, if an object is created and used entirely within a method (and not shared across threads) the JVM realizes no other thread can possibly access the object, and thus that all synchronization is redundant. In such a case, the JIT compiler simply eliminates the lock altogether.

Zero Locking Overhead: Eliminating unnecessary synchronization will also prevent the JVM from paying the cost of acquiring and releasing locks in the first place.

Higher Throughput: Dead synch can sometimes lead to a higher throughput of the application, especially if the code contains many synchronized blocks.

Take a look at this piece of code:

public void someMethod() {
    StringBuilder sb = new StringBuilder();
    synchronized (sb) {
        sb.append("Hello");
        sb.append("World");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, synchronization on sb is not necessary since the StringBuilder is used only within the someMethod and is not shared between other threads. By looking at this, the JVM can perform an escape analysis to remove the lock.

3. Lock Coarsening

What is Lock Coarsening?

Lock coarsening is an optimization wherein the JVM expands the scope of a lock to cover more chunks of code instead of continuously acquiring and releasing the lock in loops or small sections of code.

Lock Coarsening Work

If the JVM finds that a tight loop or multiple adjacent code blocks acquire and release a lock too frequently, it can coarsen the lock by taking the lock outside the loop or across several blocks of code. This makes repeated acquisition and release of the lockless expensive and enables a thread to hold the lock for more iterations.

Code Example: Lock Coarsening

Consider this code snippet:

for (int i = 0; i < 1000; i++) {
    synchronized (lock) {
        // Do something
    }
}
Enter fullscreen mode Exit fullscreen mode

Lock coarsening pushes the lock acquisition outside the loop, so the thread acquires the lock only once:

synchronized (lock) {
  for (int i = 0; i < 1000; i++) {
    // Do something
  }
}
Enter fullscreen mode Exit fullscreen mode

The JVM can dramatically improve performance by avoiding more acquires and releases of the lock.

Lock Coarsening Benefits

Less Freedom of Locking Overheads: Coarsening avoids lock acquisitions and releases, especially in hotspot code, such as loops that have been iterated thousands of times.

Improved Performance:
Locking for a longer period improves performance when compared to the scenario in which, without locking, such a lock would be acquired and released multiple times.

4. Light Weight and Heavy Weight Locks

The JVM uses two different locking techniques based on the degree of contention among the threads. Such techniques include lightweight locks and heavyweight locks.

Light Weight Locking

Lightweight locking takes place in the absence of a contention lock, meaning only one thread is trying to acquire that lock. In such scenarios, the JVM optimizes the acquisition using a CAS operation when trying to acquire the lock, which can happen without heavyweight synchronization.

Heavyweight Locking

In case multiple threads want to obtain the same lock; that is, there is contention, the JVM escalates this to heavyweight locking. This would involve blocking threads at the OS level and managing them using OS-level synchronization primitives. Heavyweight locks are slower because they actually require the OS to perform context switching, as well as to manage threads.

Lock Escalation

If contention arises at a lightweight lock, the JVM may escalate it to a heavyweight lock. Escalation here means switching from the fast, user-level lock to a more expensive, OS-level lock which includes thread blocking.

Benefits of Lightweight Locks

Rapid acquisition of a lock: When there is no contention, lightweight locks are far quicker than heavyweight locks because they avoid OS-level synchronization.

Reduced blocking: With no contentions, threads do not block and increase linearly with lower latency.

Disadvantages of Heavyweight Locks

Performance Overhead: Heavyweight locks incur the cost of thread blocking, context switching, and waking up threads with performance degradation at very high contention regimes.

All these optimizations help the JVM improve performance in multi-threaded applications, so developers can now write safe, concurrent code without sacrificing much in the way of synchronization overhead. Understanding these optimizations can help developers design more efficient systems, especially in cases that have a high-performance penalty for locking.

Top comments (0)