Java Synchronization
Two threads can overwrite each other's work when they update shared data without coordination. Synchronization lets them follow the same locking rule around the relevant work. The rule must cover the shared state, not just whichever method is easiest to mark synchronized.
What is Synchronization?
Synchronization is a mechanism that controls how multiple threads access shared resources.
When a resource is shared between multiple threads, synchronization can ensure that only one thread at a time executes a particular critical section.
For example, suppose two threads are trying to update the same bank account balance.
Without proper synchronization:
Thread 1 → reads balance
Thread 2 → reads balance
Thread 1 → updates balance
Thread 2 → updates balanceOne update may overwrite another.
With synchronization:
Thread 1 → accesses resource
↓
completes
↓
Thread 2 → accesses resourceThis helps prevent inconsistent results.
Why is Synchronization Needed?
Synchronization is mainly needed when multiple threads access shared mutable data.
Consider:
class Counter {
int count = 0;
void increment() {
count++;
}
}Now suppose two threads execute:
counter.increment();at the same time.
The statement:
count++;is not a single indivisible operation. Conceptually, it involves:
Read count
↓
Add 1
↓
Write countTwo threads can interfere with these steps.
This can produce an incorrect final value.
What is a Race Condition?
A race condition occurs when the result of a program depends on the timing or order in which multiple threads access shared data.
Example
class Counter {
int count = 0;
void increment() {
count++;
}
}Suppose the initial value is:
count = 0Two threads execute increment() simultaneously.
A possible sequence is:
Thread-1 → reads 0
Thread-2 → reads 0
Thread-1 → calculates 1
Thread-2 → calculates 1
Thread-1 → writes 1
Thread-2 → writes 1Expected result:
2Actual result:
1This is a race condition.
Synchronization can prevent both threads from entering the critical section at the same time.
Synchronized Keyword
Java provides the synchronized keyword to implement synchronization.
It can be used with:
- Instance methods
- Static methods
- Code blocks
For example:
synchronized void increment() {
count++;
}The synchronized keyword ensures that only one thread at a time can execute the synchronized instance method for the same object.
Synchronized Instance Method
A synchronized instance method locks the monitor associated with the object on which the method is invoked.
Example
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}Now multiple threads can safely call:
counter.increment();for the same Counter object.
Only one thread at a time can execute the synchronized increment() method on that object.
How Synchronized Instance Method Works
Suppose:
Counter counter = new Counter();Two threads call:
counter.increment();The monitor associated with counter controls access.
Conceptually:
Thread-1
↓
acquires counter's monitor
↓
executes increment()
↓
releases monitor
Thread-2
↓
acquires counter's monitor
↓
executes increment()The second thread must wait until the first thread releases the monitor.
Synchronized Block
Instead of synchronizing an entire method, we can synchronize only a specific section of code.
Syntax:
synchronized (object) {
// critical section
}Example
class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}Here, only the code inside the synchronized block requires the monitor.
Why Use a Synchronized Block?
Synchronizing an entire method can sometimes lock more code than necessary.
Consider:
public synchronized void process() {
// Code that does not access shared data
// Code that accesses shared data
// More code that does not access shared data
}The entire method is protected by synchronization.
Instead, we can synchronize only the critical section:
public void process() {
// Code that does not need synchronization
synchronized (this) {
// Critical section
}
// More code that does not need synchronization
}This can reduce the amount of time a lock is held.
Synchronized Static Method
A static synchronized method locks the monitor associated with the class object, not an individual instance.
Example
class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}The lock is associated with:
Counter.classConceptually:
static synchronized method
↓
Counter.class
↓
monitorThis is useful when multiple threads access shared static data.
Synchronized Static Block
We can also synchronize a block using a class object.
class Counter {
private static int count = 0;
public static void increment() {
synchronized (Counter.class) {
count++;
}
}
}Here, Counter.class is used as the monitor.
Instance Synchronization vs Static Synchronization
| Instance Synchronization | Static Synchronization |
|---|---|
| Locks an object monitor | Locks the class object's monitor |
| Used with instance methods/blocks | Used with static methods/blocks |
| Lock associated with this for a synchronized instance method | Lock associated with ClassName.class |
| Protects data associated with an object | Commonly protects shared static data |
For example:
public synchronized void method() {
}is conceptually similar to:
public void method() {
synchronized (this) {
}
}Whereas:
public static synchronized void method() {
}is conceptually similar to:
public static void method() {
synchronized (ClassName.class) {
}
}What is a Monitor?
A monitor is the synchronization mechanism associated with an object.
Every Java object can be used as a monitor for synchronized operations.
For example:
synchronized (obj) {
// critical section
}Here, obj is used as the monitor.
A thread must acquire the monitor before entering the synchronized block.
When the thread exits the synchronized block, the monitor is released.
Critical Section
A critical section is a section of code that accesses shared data and must be executed with appropriate synchronization when concurrent access could cause incorrect results.
Example
synchronized (this) {
count++;
}Here:
synchronized block
↓
critical section
↓
shared data is accessedThe goal is to prevent unsafe concurrent access to the shared state.
Synchronization and Mutual Exclusion
Synchronization provides mutual exclusion for synchronized sections protected by the same monitor.
Mutual exclusion means:
At most one thread can hold a particular monitor at a time.For example:
synchronized (lock) {
// critical section
}If Thread-1 owns the lock monitor, another thread cannot enter another synchronized block protected by the same lock until the monitor becomes available.
Using a Dedicated Lock Object
Instead of using this, we can create a dedicated object for synchronization.
Example
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}This can make the synchronization boundary more explicit.
It also avoids exposing the object's monitor as the lock used for synchronization.
Same Lock vs Different Locks
Synchronization works only when threads coordinate using the same monitor.
For example:
Object lock = new Object();
synchronized (lock) {
// protected section
}Another thread using the same lock object must wait if the first thread owns the monitor.
But different objects represent different monitors:
Object lock1 = new Object();
Object lock2 = new Object();These are separate locks.
lock1 → Monitor 1
lock2 → Monitor 2A thread holding lock1 does not prevent another thread from acquiring lock2.
Example: Thread-Safe Counter
A common example of synchronization is a counter shared between multiple threads.
Without Synchronization
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}Multiple threads can execute increment() concurrently.
This can cause a race condition.
With Synchronization
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}Now only one thread at a time can execute the synchronized increment() method for the same Counter object.
Complete Example
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class HelloWorld {
public static void main(String[] args)
throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.getCount());
}
}Output
2000Because increment() is synchronized, the updates are protected by the same object's monitor.
Synchronization Does Not Make Every Operation Thread-Safe
Adding synchronized to one method does not automatically make an entire class thread-safe.
For example:
class Counter {
private int count;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}The increment() method is protected, but thread safety depends on how the object is accessed and whether all relevant shared state is properly coordinated.
Synchronization must be designed around the actual shared state and operations.
Synchronized Methods on the Same Object
Consider:
class Test {
public synchronized void method1() {
// ...
}
public synchronized void method2() {
// ...
}
}Both methods use the same instance monitor when called on the same object.
Therefore, two threads cannot simultaneously execute these methods on the same instance.
For example:
Test obj = new Test();If Thread-1 is executing:
obj.method1();Thread-2 cannot simultaneously enter:
obj.method2();because both methods require the same object's monitor.
Synchronized Methods on Different Objects
Consider:
Test obj1 = new Test();
Test obj2 = new Test();If one thread executes:
obj1.method1();and another executes:
obj2.method1();the two methods use different object monitors.
Therefore, synchronization on obj1 does not block synchronization on obj2.
Conceptually:
obj1 → Monitor 1
obj2 → Monitor 2Synchronization and wait(), notify(), notifyAll()
Synchronization is closely related to the methods:
- wait()
- notify()
- notifyAll()
These methods belong to Object.
They are used for thread coordination and must be called while the current thread owns the relevant object's monitor.
Example
synchronized (lock) {
lock.wait();
}Another thread can notify:
synchronized (lock) {
lock.notify();
}The relationship can be summarized as:
synchronized
↓
acquire monitor
↓
wait()
↓
release monitor and wait
↓
notify()
↓
thread can compete to reacquire monitorThe detailed behavior of these methods is part of Java's inter-thread communication mechanism.
Does sleep() Release the Lock?
No. This is an important distinction.
If a thread owns a monitor and calls:
Thread.sleep(2000);it enters TIMED_WAITING, but it does not release the monitor because of sleep().
Example
synchronized (lock) {
Thread.sleep(2000);
}The thread continues to hold the monitor while sleeping.
By contrast, wait() releases the monitor of the object on which wait() was called.
Synchronization and Performance
Synchronization introduces coordination between threads and can reduce concurrency when used unnecessarily.
For example:
public synchronized void process() {
// Large amount of code
}The entire method requires the object's monitor.
If only a small portion needs protection, a synchronized block may be more appropriate:
public void process() {
// Non-critical work
synchronized (lock) {
// Critical section
}
// More non-critical work
}The goal is not to avoid synchronization completely, but to synchronize the shared state correctly and keep the protected region appropriately scoped.
Synchronization and Deadlock
Poorly designed synchronization can lead to a deadlock.
A deadlock can occur when two or more threads wait indefinitely for locks held by each other.
Example concept:
Thread-1
↓
holds Lock-A
↓
waits for Lock-B
Thread-2
↓
holds Lock-B
↓
waits for Lock-ANeither thread can continue.
Lock-A ← Thread-1
↑
|
Thread-2
|
↓
Lock-BCareful lock ordering and minimizing unnecessary nested locking can help avoid deadlocks.
Synchronization vs volatile
volatile and synchronized solve different problems.
volatile
A volatile variable provides visibility guarantees for reads and writes of that variable between threads.
private volatile boolean running = true;However, volatile does not make compound operations such as:
count++;atomic.
synchronized
synchronized provides mutual exclusion and also establishes the required memory visibility guarantees around monitor locking and unlocking.
For example:
public synchronized void increment() {
count++;
}So:
volatile → visibility of a variable
synchronized → mutual exclusion + visibility guaranteesThe correct choice depends on the problem being solved.
Synchronization vs Atomic Classes
For simple atomic updates, Java also provides classes such as:
- AtomicInteger
- AtomicLong
- AtomicBoolean
Example
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();For some simple shared-state operations, atomic classes can avoid explicit locking.
However, they are not a universal replacement for synchronization. More complex operations involving multiple pieces of state may still require locking or another coordination mechanism.
Synchronization vs Lock API
Java also provides explicit locking through the java.util.concurrent.locks package.
For example:
Lock lock = new ReentrantLock();A lock can be used like:
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}Compared with synchronized, explicit locks provide additional capabilities such as timed and interruptible lock acquisition.
For straightforward mutual exclusion, synchronized is often simpler.
Important Rules of Synchronization
Rule 1: Synchronization requires a shared monitor
Threads must synchronize using the same monitor to coordinate access.
synchronized (lock) {
// ...
}Rule 2: Instance synchronized methods use the object monitor
public synchronized void method() {
}is associated with the current object.
Rule 3: Static synchronized methods use the class monitor
public static synchronized void method() {
}is associated with the class object.
Rule 4: sleep() does not release a monitor
Thread.sleep(1000);does not release a lock held by the sleeping thread.
Rule 5: wait() releases the relevant monitor
Calling:
lock.wait();causes the current thread to release lock's monitor and wait.
Rule 6: Synchronize access to shared mutable state
Synchronization is generally relevant when multiple threads can access and modify the same state.
Common Mistakes
Mistake 1: Synchronizing Different Objects
This does not coordinate the threads:
synchronized (new Object()) {
// ...
}Every evaluation creates a different object, so threads are not using a common monitor.
Mistake 2: Calling wait() Without Owning the Monitor
This is incorrect:
lock.wait();unless the current thread owns lock's monitor.
Correct:
synchronized (lock) {
lock.wait();
}Otherwise, Java throws IllegalMonitorStateException.
Mistake 3: Assuming sleep() Releases a Lock
It does not.
synchronized (lock) {
Thread.sleep(5000);
}The thread continues to hold lock's monitor while sleeping.
Mistake 4: Synchronizing Unnecessary Code
Synchronizing large sections unnecessarily can reduce concurrency.
Prefer protecting the actual critical section where possible.
Advantages of Synchronization
- Helps prevent race conditions.
- Protects shared mutable data.
- Provides mutual exclusion for synchronized sections using the same monitor.
- Provides memory visibility guarantees associated with monitor locking and unlocking.
- Helps maintain consistent application state.
- Can be used with both instance and static members.
Disadvantages of Synchronization
- Can reduce concurrency if used excessively.
- Threads may have to wait for locks.
- Poor lock design can cause deadlocks.
- Large synchronized sections can reduce performance.
- Synchronization can make concurrent code more complex.
Key Takeaways
- Synchronization controls concurrent access to shared resources.
- It is mainly important when multiple threads access shared mutable state.
- Java provides the synchronized keyword for synchronization.
- A synchronized instance method uses the object's monitor.
- A static synchronized method uses the class object's monitor.
- A synchronized block allows only a specific section of code to be protected.
- Mutual exclusion means only one thread can own a particular monitor at a time.
- A race condition can occur when threads access shared data without proper coordination.
- sleep() does not release a monitor.
- wait() releases the monitor of the object on which it is called.
- wait(), notify(), and notifyAll() are methods of Object.
- volatile and synchronized solve different concurrency problems.
- Atomic classes can be useful for simple atomic operations.
- ReentrantLock provides an alternative explicit locking mechanism.
- Poor synchronization design can cause deadlocks.
Simple Concept to Remember
Multiple Threads
↓
Shared Resource
↓
Concurrent Access
↓
Race Condition↓ Synchronization ↓ Controlled Access ↓ Consistent Shared State
The key idea is: Synchronization coordinates access to shared mutable resources so that concurrent threads do not interfere with each other in unsafe ways.
See you in next page....🙂