When multiple threads run at the same time in Java, they often try to access the same resources — like a variable, object, or file. Without any control, this can cause unpredictable behavior and bugs that are hard to trace. That’s where the synchronized
keyword in Java comes in.
In simple words, synchronized
is a tool Java gives us to prevent multiple threads from interfering with each other while working on shared resources. Let’s break it down in a clear and practical way.
Why Do We Need the synchronized
Keyword?
Imagine two people trying to withdraw money from the same bank account at the exact same time. If both transactions run without coordination, the account might go into a negative balance.
This type of problem is called a race condition. In Java, the synchronized
keyword is used to avoid such situations by allowing only one thread at a time to access a block of code or method.
How Does synchronized
Work?
When a thread enters a synchronized
block or method, it locks the object it belongs to. Other threads trying to enter the same block or method must wait until the lock is released.
This locking mechanism ensures thread safety, but it also slows things down if used too often. That’s why it’s important to use it wisely.
Types of Synchronization in Java
There are two main ways to use the synchronized
keyword in Java:
- Synchronized Method — Entire method is synchronized.
- Synchronized Block — Only a specific part of the code is synchronized.
Let’s look at both with examples.
Synchronized Method
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Two threads incrementing the counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.getCount());
}
}
- The
increment()
method is marked assynchronized
. - This means only one thread can execute it at a time.
- Without synchronization, the final count might not be 2000 due to race conditions.
- With synchronization, the output will always be 2000.
Run the program twice: once with synchronization enabled and once without. Compare the outputs to observe the effect of synchronization.
In this case, we will always get 2000 as the output in both scenarios.
Why the Result Can Still Be the Same (2000) Without Synchronization
When two threads increment the counter (count++
), here’s what happens under the hood:
count++
is not atomic. It actually breaks down into three steps:
- Read the current value of
count
. - Add
1
to it. - Write the new value back to memory.
If two threads interleave at the wrong time, one update can overwrite the other. That’s the race condition.
But… race conditions don’t guarantee wrong results every single run. Sometimes:
- The threads happen to run sequentially (one finishes a batch before the other interferes).
- The CPU scheduler doesn’t interleave them in a conflicting way.
- The number of operations is small, so the timing never collides.
In those cases, you might still get the “correct” result of 2000 by luck, even though the code isn’t thread-safe.
Why It’s Dangerous
The key point: the result is non-deterministic.
- You might run the program 10 times and see 2000 each time.
- But on the 11th run, you might get 1987 or 1995.
The behavior depends on CPU scheduling, thread timing, and hardware. That’s why without synchronization, the program is unsafe even if it sometimes looks fine.
How to Force the Wrong Behavior (to See the Bug)
If you want to actually see the race condition happen more often:
- Increase the loop count (e.g.,
1000000
instead of1000
). - Run on a machine with multiple cores.
- Add artificial delays (like
Thread.yield()
inside the loop).
You’ll quickly notice results less than 2000 when threads interfere.
Without
synchronized
, getting 2000 doesn’t mean the code is correct — it just means the timing didn’t trigger a race condition in that run. Synchronization guarantees correctness every time, not just by chance.
Synchronized Block
Sometimes, we don’t need to synchronize an entire method — just a small critical section of code. That’s where synchronized blocks are useful.
class Printer {
public void printMessage(String message) {
synchronized(this) {
System.out.print("[" + message);
try {
Thread.sleep(100); // Simulate delay
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("]");
}
}
}
public class SyncBlockExample {
public static void main(String[] args) {
Printer printer = new Printer();
Thread t1 = new Thread(() -> printer.printMessage("Hello"));
Thread t2 = new Thread(() -> printer.printMessage("World"));
t1.start();
t2.start();
}
}
- Only the block inside
synchronized(this)
is locked. - This ensures that printing of messages happens in a safe, consistent way (e.g.,
[Hello]
and[World]
, instead of jumbled outputs). - Synchronizing just the critical section improves performance compared to locking the whole method.
Static Synchronization
If a method is declared as static synchronized
, the lock is placed on the class object rather than the instance. This is useful when you want synchronization across all instances of a class.
class SharedResource {
public static synchronized void showMessage(String msg) {
System.out.println("Message: " + msg);
}
}
Here, only one thread across all objects of SharedResource
can access showMessage()
at a time.
Pros and Cons of Using synchronized
Advantages
- Prevents race conditions.
- Ensures data consistency.
- Provides a simple way to handle multi-threading issues.
Disadvantages
- Can reduce performance because of thread blocking.
- May lead to deadlocks if not handled carefully.
- In large-scale systems, too much synchronization can become a bottleneck.
Best Practices for Using synchronized
- Synchronize only the critical section, not the entire method, when possible.
- Keep synchronized blocks short and efficient.
- Avoid nested synchronization to reduce deadlock risks.
- Consider higher-level concurrency tools like
ReentrantLock
orjava.util.concurrent
classes for complex scenarios.
Conclusion
The synchronized keyword in Java is a powerful tool to ensure thread safety. It allows you to control how multiple threads interact with shared resources, preventing errors like race conditions.
However, it’s not always the most efficient choice. Use it when necessary, but also explore modern concurrency utilities for more flexible and performant solutions.
If you’re just starting with multithreading in Java, mastering synchronized
is the first step toward writing safe, concurrent programs.