- What is Synchronization?
- Why is Synchronization Needed?
- Example Without Synchronization:
- Expected Output:
- Actual Output:
- Real-World Example (Bank ATM Withdrawal)
- Output Without Synchronization:
- Solution: Use Synchronization
- Output After Synchronization:
- Key Takeaways
- The synchronized Keyword
- 1. Synchronized Method
- Syntax of Synchronized Method
- Java Program
- Behind the Scenes:
- 2. Synchronized Block
- Syntax of Synchronized Block
- Java Program
- Synchronized on Custom Objects
- How Synchronization Works Internally(In short)
- Synchronized Method vs Synchronized Block
- Static Synchronization in Java
- What Happens in Static Synchronized Methods?
- Key Point:
- Why Is That Important?
- Reentrant Synchronization
- What is Reentrancy?
- Example:
- What’s Happening?
- Why Is Reentrancy Important?
- Best Practices for Synchronization
- Common Interview Questions
- 1. What is synchronization in Java?
- 2. Difference between synchronized method and block?
- 3. Can we synchronize static methods?
- 4. What is a monitor lock?
- 5. What is reentrant synchronization?
- 6. Difference between synchronized and ReentrantLock?
- Conclusion
In a multithreaded environment, multiple threads can access shared resources (like variables, objects, or files) at the same time. This can lead to data inconsistency, unexpected results, or even program crashes.
To prevent such problems, Java provides a powerful mechanism called Synchronization.
What is Synchronization?
Synchronization is the process of controlling access to shared resources by multiple threads in a multithreaded environment. It ensures that only one thread can access the critical section (a block of code that modifies shared data) at a time.
Think of it like a one-person-at-a-time restroom, only one person (thread) can enter at a time to avoid clashes.
Why is Synchronization Needed?
Let’s understand with an example.
Example Without Synchronization:
class Counter {
int count = 0;
void increment() {
count++; // Critical Section
}
}
public class TestWithoutSync extends Thread {
Counter counter;
TestWithoutSync(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
TestWithoutSync t1 = new TestWithoutSync(counter);
TestWithoutSync t2 = new TestWithoutSync(counter);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.count);
}
}
Expected Output:
Final Count: 2000
Actual Output:
Final Count: 1876 (or some random value)
❓ Why This Happens?
The count++ operation is not atomic. It actually involves 3 steps:
- Read the current value of
count - Increment it by 1
- Write the new value back
Now imagine two threads doing this at the same time — they might both read the same value and overwrite each other’s result.
This is called a race condition — the output depends on the unpredictable “race” between threads.
Real-World Example (Bank ATM Withdrawal)
Let’s say two people are trying to withdraw money from the same bank account at the same time from different ATMs.
class BankAccount {
int balance = 1000;
void withdraw(int amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " is withdrawing " + amount);
balance = balance - amount;
System.out.println(Thread.currentThread().getName() + " has completed withdrawal.");
} else {
System.out.println(Thread.currentThread().getName() + " - Insufficient balance");
}
}
}
public class BankTest extends Thread {
BankAccount account;
BankTest(BankAccount account) {
this.account = account;
}
public void run() {
account.withdraw(700);
}
public static void main(String[] args) {
BankAccount account = new BankAccount();
BankTest user1 = new BankTest(account);
BankTest user2 = new BankTest(account);
user1.setName("User1");
user2.setName("User2");
user1.start();
user2.start();
}
}
Output Without Synchronization:
Both users might end up withdrawing 700 each, resulting in negative balance, which is logically incorrect.
Solution: Use Synchronization
Let’s fix our first example using synchronized:
class Counter {
int count = 0;
synchronized void increment() {
count++;
}
}
Now, when one thread is executing increment(), the other thread has to wait. This ensures thread safety.
Output After Synchronization:
Now the final count will always be:
Final Count: 2000
Key Takeaways
count++is not a thread-safe operation.- Without synchronization, multiple threads can cause data inconsistency.
- Use
synchronizedkeyword to protect critical sections. - Synchronization ensures only one thread accesses shared resource at a time.
The synchronized Keyword
Java provides the synchronized keyword to lock a method or block so that only one thread can access it at a time.
There are two main ways to use it:
1. Synchronized Method
When you mark a method as synchronized, the entire method becomes a critical section.
Syntax of Synchronized Method
synchronized void myMethod() {
// Critical section
}
This locks the entire method for one thread at a time.
Java Program
class Printer {
synchronized void printDocument(String doc) {
System.out.println(Thread.currentThread().getName() + " is printing " + doc);
try {
Thread.sleep(1000); // Simulate print time
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished printing " + doc);
}
}
public class SyncMethodExample extends Thread {
Printer printer;
String doc;
SyncMethodExample(Printer printer, String doc) {
this.printer = printer;
this.doc = doc;
}
public void run() {
printer.printDocument(doc);
}
public static void main(String[] args) {
Printer printer = new Printer();
SyncMethodExample t1 = new SyncMethodExample(printer, "Resume.pdf");
SyncMethodExample t2 = new SyncMethodExample(printer, "Invoice.docx");
t1.start();
t2.start();
}
}
Output:
Thread-0 is printing Resume.pdf
Thread-0 finished printing Resume.pdf
Thread-1 is printing Invoice.docx
Thread-1 finished printing Invoice.docx
Only one thread prints at a time — the entire method is locked.
Behind the Scenes:
- For non-static synchronized methods, the lock is acquired on the current object (
this). - For static synchronized methods, the lock is acquired on the class object (
ClassName.class).
2. Synchronized Block
Synchronized blocks let you lock only a portion of your method — offering more fine-grained control and better performance.
Syntax of Synchronized Block
void myMethod() {
synchronized(this) {
// Critical section
}
}
Java Program
class Printer {
void printDocument(String doc) {
System.out.println(Thread.currentThread().getName() + " is preparing to print " + doc);
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " is printing " + doc);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished printing " + doc);
}
System.out.println(Thread.currentThread().getName() + " is doing post-print work...");
}
}
public class SyncMethodExample extends Thread {
Printer printer;
String doc;
SyncMethodExample(Printer printer, String doc) {
this.printer = printer;
this.doc = doc;
}
public void run() {
printer.printDocument(doc);
}
public static void main(String[] args) {
Printer printer = new Printer();
SyncMethodExample t1 = new SyncMethodExample(printer, "Resume.pdf");
SyncMethodExample t2 = new SyncMethodExample(printer, "Invoice.docx");
t1.start();
t2.start();
}
}
This locks only a specific block of code, giving you more control and better performance.
Synchronized on Custom Objects
You can also synchronize using custom lock objects instead of this:
public class SharedPrinter {
private final Object lock = new Object();
void print(String doc) {
synchronized (lock) {
System.out.println("Printing: " + doc);
}
}
}
Using custom lock objects can help you:
- Synchronize specific portions of a class
- Avoid blocking unrelated operations
- Separate multiple locks within the same class
How Synchronization Works Internally(In short)
Every Java object has a monitor lock (aka intrinsic lock). When a thread enters a synchronized block or method, it acquires the lock. Other threads trying to access the same block will wait until the lock is released.
Synchronized Method vs Synchronized Block
| Feature | Synchronized Method | Synchronized Block |
|---|---|---|
| Lock Scope | Whole method | Specific block of code |
| Performance | Slower (locks everything) | Faster (locks only needed part) |
| Flexibility | Less flexible | More flexible |
Static Synchronization in Java
We know that the synchronized keyword ensures only one thread at a time can access a synchronized method or block. But did you know that static methods can also be synchronized?
What Happens in Static Synchronized Methods?
When you declare a static method as synchronized, Java locks the Class object, not the instance of the class.
Static methods can also be synchronized.
class Counter {
static int count = 0;
public static synchronized void increment() {
count++;
}
}
⚠️ Here, the class-level lock is acquired, not the object-level lock.
Key Point:
- For non-static synchronized methods → lock is on the object (
this) - For static synchronized methods → lock is on the class object (
Counter.class)
Why Is That Important?
Because all instances of the class share the same static method and variable, we must lock at the class level to prevent race conditions across all threads, regardless of the object they belong to.
Reentrant Synchronization
Now let’s talk about Reentrant Synchronization, a feature that makes Java’s synchronized blocks and methods more flexible and powerful. Java uses Reentrant Locks. This means a thread can re-enter the same lock if it already holds it.
What is Reentrancy?
Java locks are reentrant, meaning a thread that already holds a lock can acquire it again without blocking itself.
In simple words:
If you already have the key to the door, you can go through another door that requires the same key — no one stops you.
Example:
class Demo {
synchronized void method1() {
System.out.println("Inside method1");
method2(); // Same thread enters another synchronized method
}
synchronized void method2() {
System.out.println("Inside method2");
}
}
No deadlock occurs because the thread is allowed to re-enter.
What’s Happening?
- A thread calls
method1()and acquires the object-level lock. - Inside
method1(), it callsmethod2()— which is also synchronized. - Because the same thread already owns the lock, it is allowed to enter
method2()without waiting. - No deadlock occurs.
Why Is Reentrancy Important?
Without reentrant locks:
- A thread could block itself, waiting for a lock it already holds, causing a deadlock!
This design allows for modular and nested synchronized methods, which is common in real-world applications.
Best Practices for Synchronization
✅ Only synchronize critical sections (not entire methods if unnecessary)
✅ Avoid nested synchronization (can lead to deadlocks)
✅ Prefer using concurrent collections like ConcurrentHashMap
✅ Use volatile for visibility when full synchronization isn’t required
✅ Consider using higher-level constructs like AtomicInteger, Semaphore, or ReentrantLock
Common Interview Questions
1. What is synchronization in Java?
Synchronization is a mechanism that ensures only one thread can access a shared resource (like a variable or method) at a time to prevent data inconsistency and race conditions.
2. Difference between synchronized method and block?
| Synchronized Method | Synchronized Block |
|---|---|
| Locks the entire method | Locks a specific section of code |
| Less control | More control and performance |
Automatically uses this as lock | Can lock on any object |
3. Can we synchronize static methods?
Yes. When a static method is synchronized, the lock is acquired on the Class object (e.g., ClassName.class), not on any instance.
4. What is a monitor lock?
A monitor lock (or intrinsic lock) is a lock associated with every Java object. It is used by threads to gain exclusive access to synchronized methods or blocks.
5. What is reentrant synchronization?
Reentrant synchronization means a thread can re-acquire the same lock it already holds. This allows nested synchronized calls without blocking itself.
6. Difference between synchronized and ReentrantLock?
synchronized | ReentrantLock |
|---|---|
| Keyword-based, simpler | Class-based, more flexible |
| Automatically releases lock | Must manually lock() and unlock() |
| No try-lock or timeout support | Supports try-lock, timeout, fairness |
| Less control | More advanced features |
Conclusion
Synchronization is one of the most important concepts in Java multithreading. It helps prevent race conditions, ensures data consistency, and keeps your applications safe and reliable.