Demystifying the synchronized Keyword in Java
If you’ve ever written multi-threaded code in Java, you’ve likely stumbled upon the synchronized keyword. At first glance, it looks like a magic wand to solve all concurrency problems—but as with most "magical" solutions, it comes with caveats.
In this post, we’ll break down what synchronized really does, when it works, when it doesn’t, and weigh its pros and cons.
🔑 What Does synchronized Do?
The synchronized keyword is Java’s built-in way to ensure mutual exclusion. In simple terms:
👉 Only one thread at a time can execute a synchronized method or block that’s locked on the same object.
It achieves this by acquiring a monitor lock (also called an intrinsic lock) on the object. Other threads trying to acquire the same lock are forced to wait until it’s released.
✅ When Does synchronized Work?
synchronized is your friend when:
- Multiple threads are accessing and modifying shared data.
- You want to prevent race conditions.
- You need to ensure memory visibility (changes made by one thread become visible to others).
You are protecting either:
- Instance methods → lock is taken on this.
- *Static methods *→ lock is taken on the class object.
Code blocks → lock is taken on any object you specify (synchronized(someObject) { ... }).
❌ When Doesn’t It Help?
synchronized isn’t a silver bullet. It does not help if:
- Threads synchronize on different objects (wrong lock = no safety).
- You’re dealing with deadlocks (threads waiting on each other’s locks).
- Performance is critical—too much contention slows things down.
- You’re working with non-shared resources (local variables don’t need synchronization).
- You need synchronization across JVMs—synchronized only works within one JVM.
Aspect | Pros ✅ | Cons ❌ |
---|---|---|
Thread Safety | Ensures only one thread accesses shared resource at a time. | Incorrect use (wrong lock) gives false sense of safety. |
Simplicity | Very easy to use (synchronized keyword is enough). |
Can be misused easily, leading to bugs. |
Memory Visibility | Guarantees changes by one thread are visible to others. | Doesn’t prevent logical errors like deadlock or starvation. |
Flexibility | Can synchronize methods or specific code blocks. | Synchronizing too broadly (e.g., whole method) reduces performance. |
Reliability | Built-in, well-tested by JVM, no extra libraries needed. | Performance bottleneck if many threads contend for same lock. |
Granularity | Works well for small critical sections. | Coarse-grained locking makes code slower and less scalable. |
👩💻 Example: Counter Without and With synchronized
❌ Without Synchronization (Race Condition)
class Counter {
private int count = 0;
public void increment() {
count++; // not thread-safe
}
public int getCount() {
return count;
}
}
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count (expected 2000): " + counter.getCount());
}
}
Output -> 👉 You’ll often get a result less than 2000 because both threads modify count at the same time, causing lost updates.
Why?
Let's look at the Timeline Graph
Time ↓ | T1 | T2 |
---|---|---|
t1 | BEGIN | |
t2 | READ count = 0 | |
t3 | READ count = 0 | |
t4 | UPDATE count = 1 | |
t5 | UPDATE count = 1 (overwrites T1) | |
t6 | COMMIT (Final count = 1, lost update) | COMMIT |
✅ With Synchronization
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // now thread-safe
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count (expected 2000): " + counter.getCount());
}
}
Output -> 👉 This time you’ll reliably get 2000, because only one thread at a time can enter the increment() method.
How?
Timeline Graph
Time ↓ | T1 | T2 |
---|---|---|
t1 | ENTER synchronized block (lock acquired) | |
t2 | READ count = 0, UPDATE count = 1 | |
t3 | EXIT synchronized block (lock released) | |
t4 | ENTER synchronized block (lock acquired) | |
t5 | READ count = 1, UPDATE count = 2 | |
t6 | EXIT synchronized block (lock released) |
Top comments (0)