Understanding Java Synchronized: Basic Usage, Underlying Mechanism, and Execution Results
This article explains the three forms of using the synchronized keyword in Java—method, static method, and block synchronization—demonstrates each with runnable code examples, analyzes the JVM monitorenter/monitorexit instructions, and clarifies why synchronized methods and blocks enforce mutual exclusion and visibility guarantees.
1. Basic Usage of Synchronized
Synchronized is the most common and simplest way to solve concurrency problems in Java. It mainly serves three purposes: ensuring mutual exclusion when accessing synchronized code, guaranteeing timely visibility of shared variable modifications, and effectively preventing instruction reordering.
There are three syntactic forms of synchronized:
Synchronizing an instance method
Synchronizing a static method
Synchronizing a code block
The following examples illustrate each form while keeping the rest of the code identical for easy comparison.
1) No synchronization
Code snippet 1:
package com.paddx.test.concurrent;
public class SynchronizedTest {
public void method1() {
System.out.println("Method 1 start");
try {
System.out.println("Method 1 execute");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public void method2() {
System.out.println("Method 2 start");
try {
System.out.println("Method 2 execute");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() { test.method1(); }
}).start();
new Thread(new Runnable() {
@Override
public void run() { test.method2(); }
}).start();
}
}Result: Both threads run concurrently; method2 finishes earlier because its sleep time is shorter.
2) Synchronizing an instance method
Code snippet 2 (the only change is the synchronized modifier on the methods):
package com.paddx.test.concurrent;
public class SynchronizedTest {
public synchronized void method1() { /* same body as before */ }
public synchronized void method2() { /* same body as before */ }
// main method unchanged
}Result: method2 must wait for method1 to complete because both synchronized methods lock the same object monitor.
3) Synchronizing static methods
Code snippet 3 (static synchronized methods):
package com.paddx.test.concurrent;
public class SynchronizedTest {
public static synchronized void method1() { /* same body */ }
public static synchronized void method2() { /* same body */ }
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
final SynchronizedTest test2 = new SynchronizedTest();
new Thread(() -> test.method1()).start();
new Thread(() -> test2.method2()).start();
}
}Result: Even though test and test2 are different instances, static synchronization locks the Class object, so the methods execute sequentially.
4) Synchronizing a code block
Code snippet 4 (synchronized block on this inside ordinary methods):
package com.paddx.test.concurrent;
public class SynchronizedTest {
public void method1() {
System.out.println("Method 1 start");
try {
synchronized (this) {
System.out.println("Method 1 execute");
Thread.sleep(3000);
}
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Method 1 end");
}
public void method2() {
System.out.println("Method 2 start");
try {
synchronized (this) {
System.out.println("Method 2 execute");
Thread.sleep(1000);
}
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Method 2 end");
}
// main method unchanged
}Result: Thread 2 enters its method but must wait before entering the synchronized block because both blocks lock the same object monitor.
2. Underlying Mechanism of Synchronized
To understand the execution results, we look at the JVM bytecode. Decompiling a synchronized block yields the monitorenter and monitorexit instructions:
package com.paddx.test.concurrent;
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}The JVM specification describes monitorenter as acquiring the monitor associated with an object. If the monitor’s entry count is zero, the thread becomes the owner; if the thread already owns it, the count increments; otherwise the thread blocks until the monitor becomes free.
monitorexit requires the executing thread to be the monitor’s owner, decrements the entry count, and releases the monitor when the count reaches zero, allowing blocked threads to attempt acquisition.
For synchronized methods, the compiler adds the ACC_SYNCHRONIZED flag to the method’s access flags. At runtime, the JVM checks this flag, acquires the appropriate monitor (object monitor for instance methods, class monitor for static methods), executes the method body, and then releases the monitor.
3. Explanation of Execution Results
With the monitor semantics in mind:
In snippet 2, both synchronized instance methods lock the same object monitor, so they run sequentially.
In snippet 3, static synchronized methods lock the class monitor, causing sequential execution even across different instances.
In snippet 4, synchronized blocks lock this , which is the same object for both methods, thus the blocks execute one after the other.
4. Summary
Synchronized is the most widely used tool for ensuring thread safety in Java. Understanding its underlying monitor mechanism helps developers use it correctly and choose more appropriate concurrency strategies when needed, leading to more robust multithreaded applications.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.