The JVM’s Just-In-Time (JIT) compiler can sometimes eliminate synchronization primitives entirely, even when they appear to be present in your bytecode, because it can prove they’re unnecessary.

Let’s see this in action. Imagine you have a simple counter that’s accessed concurrently by multiple threads.

public class SafeCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

If you were to inspect the bytecode for increment(), you’d see monitorenter and monitorexit instructions, indicating a lock is acquired and released.

public synchronized void increment();
  Code:
   0: aload_0
   1: dup
   2: astore_1
   3: monitorenter  // Lock acquired
   4: aload_0
   5: dup
   6: getfield      #2 // Field count:I
   9: iconst_1
  10: iadd
  11: putfield      #2 // Field count:I
  14: aload_0
  15: monitorexit   // Lock released
  16: goto          24
  19: astore_2
  20: aload_0
  21: monitorexit   // Lock released on exception
  22: aload_2
  23: athrow
  24: return
}

Now, consider this slightly different scenario. What if you have a method that looks like it needs synchronization, but the JIT can prove it doesn’t?

public class LockElisionExample {
    private int value = 0;

    public void updateValue(int newValue) {
        // This looks like it might need synchronization,
        // but the JIT can often elide the lock.
        synchronized (this) {
            this.value = newValue;
        }
    }

    public int getValue() {
        return value;
    }
}

When the JIT compiler analyzes updateValue, it sees that the synchronized block is on this. If it can determine that no other thread can possibly be accessing this at the same time that updateValue is executing, it can remove the monitorenter and monitorexit instructions. This is called lock elision.

The JIT does this by performing escape analysis. If an object (or in this case, this) does not "escape" the current thread of execution, the JIT can make optimizations. "Escaping" means that a reference to the object is passed to another thread, is stored in a globally accessible location (like a static field), or is otherwise made visible outside the current thread’s scope.

In updateValue, the synchronized (this) block is operating on the LockElisionExample instance itself. If the LockElisionExample instance is created and only used within a single thread, or if updateValue is called in such a way that no other thread can possibly be executing any synchronized method on the same instance concurrently, the JIT can safely remove the synchronization. It essentially figures out that the lock is never contested.

The primary benefit of lock elision is performance. Synchronization is an expensive operation, involving atomic operations and potentially context switches. By removing unnecessary locks, the JIT can significantly speed up your code. This is particularly impactful in highly concurrent applications where synchronization overhead can become a bottleneck. The JIT compiler makes these decisions at runtime, based on observed execution patterns and static analysis, meaning a piece of code might be synchronized one moment and unsynchronized the next if the execution context changes.

The one thing many developers don’t realize is that the JIT is quite aggressive with this optimization. It doesn’t just look at the immediate synchronized block; it analyzes the entire method and even considers the object’s lifecycle and accessibility. If it can prove no data race can occur, it will remove the lock. This means you can sometimes write code that looks synchronized, and the JVM will optimize it to be as fast as unsynchronized code, without you having to do anything special.

The next thing you’ll encounter when dealing with JIT optimizations is how to influence or observe these decisions, particularly with more complex synchronization scenarios.

Want structured learning?

Take the full Jvm course →