Fundamentals 27 min read

Mastering Java’s synchronized: How It Works, Optimizations & Best Practices

This article explains the Java synchronized keyword, covering its purpose for thread safety, usage on methods and blocks, underlying monitor lock mechanism, JVM bytecode details, lock optimizations such as spin, bias, lightweight and heavyweight locks, and practical examples including singleton implementation and differences from volatile.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Mastering Java’s synchronized: How It Works, Optimizations & Best Practices

Thread Safety

Before introducing synchronized , we emphasize what thread safety means: when multiple threads access an object simultaneously without needing extra synchronization, the object's behavior yields correct results, it is thread‑safe.

When multiple threads access an object simultaneously without needing extra synchronization, the object's behavior yields correct results, it is thread‑safe.

What is the synchronized keyword?

In early Java versions synchronized was a heavyweight lock with low efficiency, but since Java 6 the JVM has heavily optimized it, making it performant and widely used in frameworks and JDK source.

Ways to use synchronized

The synchronized keyword can be applied to instance methods, static methods, and code blocks. It always locks an object, so a lock object must be provided.

Instance method

Static method

Code block

1. Instance method

Example:

<code>public class SynchronizedTest1 {
    public synchronized void test() {
        System.out.println("synchronized 修饰 方法");
    }
}</code>

The lock object is the instance itself.

Constructor cannot be synchronized because it is already thread‑safe.

2. Static method

Example:

<code>public static synchronized void test() {
    i++;
}</code>

The lock object is the Class object of the declaring class, shared by all instances.

If one thread calls an instance synchronized method while another calls a static synchronized method of the same class, they do not block each other because they lock different objects.

3. Code block

Example:

<code>public class SynchronizedTest2 {
    public void test() {
        synchronized (this) {
            System.out.println("synchronized 修饰 代码块");
        }
    }
}</code>

The lock object is the object passed to synchronized . Using this locks the current instance; using Class.class locks the Class object; locking a String constant is discouraged because of the string‑intern pool sharing.

Underlying mechanism of synchronized

At the bytecode level, synchronized methods have the ACC_SYNCHRONIZED flag, which causes the JVM to acquire the monitor lock before execution and release it after the method returns or throws an exception.

Example bytecode for SynchronizedTest1 shows the flag and monitor usage.

<code>public com.zj.ideaprojects.demo.test2.SynchronizedTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1 // Method java/lang/Object."<init>":()V
        4: return

public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc #3 // String synchronized 修饰 方法
        5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
</code>

The monitor lock is an ObjectMonitor associated with each object. It guarantees that only one thread can execute the critical section at a time and provides wait / notify functionality.

Java object memory layout

Each object consists of an object header (Mark Word and Class Pointer), instance data, and optional padding. The Mark Word stores lock state, hash code, GC age, etc. When a heavyweight lock is used, the Mark Word holds a pointer to the monitor object.

32‑bit Mark Word layout:

64‑bit Mark Word layout:

Lock optimizations

JDK 1.6 introduced several optimizations to reduce the cost of synchronized :

Spin lock

When contention is short, a thread may spin briefly instead of blocking, reducing kernel transitions. The default spin count is 10 and can be tuned with -XX:PreBlockSpin . Adaptive spin locks adjust the count based on past behavior.

Lock coarsening

The JVM may enlarge the lock scope, merging multiple adjacent lock acquisitions into a single one, e.g., moving a synchronized StringBuffer.append loop lock outside the loop.

<code>public class LockCoarseningTest {
    public String test() {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("test");
        }
        return sb.toString();
    }
}</code>

Lock elimination

Escape analysis can determine that an object is not shared and remove its lock entirely.

<code>public class LockEliminateTest {
    static int i = 0;
    public void method2() {
        Object obj = new Object();
        synchronized (obj) {
            i++;
        }
    }
}</code>

Lock inflation (bias → lightweight → heavyweight)

Locks can inflate from no‑lock to biased lock, then lightweight, and finally heavyweight depending on contention. Biased locks favor the first thread that acquires them, avoiding CAS overhead when there is no competition.

Lightweight locks use CAS on the Mark Word to acquire the lock without kernel involvement; if contention occurs they inflate to heavyweight locks that rely on OS mutexes.

Singleton implementation with synchronized

<code>/**
 * Lazy‑initialized double‑checked locking singleton
 */
public class SingleDoubleCheck {
    private static volatile SingleDoubleCheck instance = null;
    private SingleDoubleCheck() {}
    public static SingleDoubleCheck getInstance() {
        if (instance == null) {
            synchronized (SingleDoubleCheck.class) {
                if (instance == null) {
                    instance = new SingleDoubleCheck();
                }
            }
        }
        return instance;
    }
}</code>

synchronized vs volatile

volatile provides visibility with lower overhead but cannot guarantee atomicity.

synchronized provides both visibility and atomicity and can lock methods or blocks.

volatile applies only to fields; synchronized can be applied to methods and code blocks.

Key takeaways

Atomicity: synchronized ensures exclusive access.

Visibility: changes are flushed to main memory on lock release.

Ordering: synchronized prevents reordering of code inside the block.

It is a pessimistic, exclusive, non‑fair lock that supports re‑entrance.

JavaconcurrencyThread Safetylock optimizationsynchronizedmonitor lock
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

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.