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 t1
synchronizes onflag
and then goes to sleep.Thread t2
reassignsflag
to a newBoolean
object whilet1
is asleep.- When
t1
wakes 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
lock
object (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
flag
variable is now a primitiveboolean
instead of aBoolean
object. This eliminates the risk of accidentally reassigning theflag
object and causing synchronization issues. - Synchronization on Lock Object: Both threads synchronize on the same
lock
object, ensuring that they properly coordinate their actions. - Notification of Waiting Threads:
t2
callslock.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
synchronized
keyword 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.