What is Synchronization in Java?
Synchronization in Java is a way to control access to shared resources in a multi-threaded environment. When multiple threads try to access the same resource (like a variable or a method), synchronization ensures that only one thread can access that resource at a time. This prevents race conditions, where the outcome of operations depends on the unpredictable timing of threads.
The synchronized Keyword
Java provides the synchronized keyword to help manage thread access. When you use synchronized, you are telling Java to lock the resource so only one thread can access it at a time.
Monitor Locks
Every object in Java has an associated monitor lock (often just called a "monitor"). When a thread enters a synchronized block or method, it acquires the monitor for the object. This means no other thread can enter any synchronized method/block on that object until the first thread releases the monitor by exiting the synchronized block/method.
Example: Synchronizing Methods on this Object
Let's consider a class Employee:
class Employee {
private String name;
// Method synchronized on 'this' object
public synchronized void setName(String name) {
this.name = name;
}
// Another method synchronized on 'this' object
public synchronized void resetName() {
this.name = "";
}
// Synchronized block inside the method
// This is equivalent of adding synchronized in method definition
public String getName() {
synchronized (this) { // Explicitly synchronizing on 'this'
return this.name;
}
}
}
Explanation:setName and resetName are synchronized methods, so they automatically synchronize on the current instance of Employee (referred to as this).getName uses a synchronized block, also synchronizing on this. This block only allows one thread to execute it at a time.
Scenario: If three different threads try to call setName, resetName, and getName simultaneously on the same Employee object, only one will proceed at a time, while the others wait.
Synchronizing on a Different Object
Now, let's modify the Employee class to use a different object for synchronization:
class Employee {
private String name;
private final Object lock = new Object();
// Method synchronized on 'this' object
public synchronized void setName(String name) {
this.name = name;
}
// Another method synchronized on 'this' object
public synchronized void resetName() {
this.name = "";
}
// Synchronized on a different object 'lock'
public String getName() {
synchronized (lock) { // Synchronizing on a different object
return this.name;
}
}
}
Explanation:setName and resetName are still synchronized on the this object.getName is now synchronized on a different object called lock.
Scenario: If two threads call setName and resetName, they will block each other because they synchronize on the same this object. However, if another thread calls getName, it won’t be blocked by setName or resetName because it synchronizes on a different object (lock).
Monitor Lock Behavior and Re-entrancy
When a thread holds a monitor lock, it can re-enter any synchronized method/block that synchronizes on the same monitor without getting blocked. This is known as re-entrancy.
Common Pitfall: Reassigning a Synchronized Object
class IncorrectSynchronization {
Boolean flag = new Boolean(true);
public void example() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (flag) {
try {
while (flag) {
System.out.println("Thread 1: about to sleep");
Thread.sleep(5000);
System.out.println("Thread 1: woke up and about to wait");
flag.wait(); // Waiting on flag
}
} catch (InterruptedException e) {
// Handle exception
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
flag = false; // Reassigning the flag object
System.out.println("Thread 2: Boolean reassigned.");
});
t1.start();
Thread.sleep(1000); // Let t1 acquire the lock first
t2.start();
t1.join();
t2.join();
}
public static void main(String[] args) throws InterruptedException {
IncorrectSynchronization incorrectSync = new IncorrectSynchronization();
incorrectSync.example();
}
}
Explanation:
Thread t1synchronizes onflagand then goes to sleep.Thread t2reassignsflagto a newBooleanobject whilet1is asleep.- When
t1wakes up and tries to callwait()onflag, it is now pointing to a different object, causing anIllegalMonitorStateException.
Correct Method
class CorrectSynchronization {
// Use a separate, immutable lock object
private final Object lock = new Object();
private boolean flag = true;
public void example() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) { // Synchronize on the lock object
try {
while (flag) {
System.out.println("Thread 1: about to sleep");
Thread.sleep(5000);
System.out.println("Thread 1: woke up and about to wait");
lock.wait(); // Wait on the lock object
}
} catch (InterruptedException e) {
// Handle exception
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) { // Synchronize on the same lock object
flag = false; // Update the flag
System.out.println("Thread 2: flag set to false.");
lock.notify(); // Notify waiting threads
}
});
t1.start();
Thread.sleep(1000); // Let t1 acquire the lock first
t2.start();
t1.join();
t2.join();
}
public static void main(String[] args) throws InterruptedException {
CorrectSynchronization correctSync = new CorrectSynchronization();
correctSync.example();
}
}
Key Changes and Explanation:
- Separate Lock Object: Introduced a separate, immutable
lockobject (private final Object lock = new Object();) used solely for synchronization. This object is never reassigned, so it remains consistent across all synchronized blocks. - Primitive Flag: The
flagvariable is now a primitivebooleaninstead of aBooleanobject. This eliminates the risk of accidentally reassigning theflagobject and causing synchronization issues. - Synchronization on Lock Object: Both threads synchronize on the same
lockobject, ensuring that they properly coordinate their actions. - Notification of Waiting Threads:
t2callslock.notify()after updatingflag, which wakes up any thread that is waiting onlock(in this case,t1).
Benefits:
- Consistency: The lock object (
lock) is never reassigned, ensuring that all threads synchronize on the same object. - Thread Safety: The flag is safely updated and checked within a synchronized block, preventing race conditions and ensuring proper coordination between threads.
Key Takeaways
synchronizedkeyword ensures that only one thread can access a block/method at a time.- Monitor locks are automatically managed by Java when you use
synchronized. - Be careful not to change the object you're synchronizing on, as it can lead to exceptions.