Java's synchronized
keyword is a fundamental mechanism for ensuring thread safety, acting as a powerful tool to prevent race conditions by controlling access to shared resources. It essentially locks a method or a specific block of code, ensuring that only one thread can execute it at a time.
Understanding Java's Synchronized Mechanism
At its core, synchronized
is about mutual exclusion and memory visibility, critical aspects of concurrent programming. When multiple threads try to access and modify the same shared data simultaneously, it can lead to inconsistent and unpredictable results – known as race conditions. The synchronized
keyword provides a way to establish a "critical section" where shared data can be accessed safely.
How Synchronized Works Under the Hood: Intrinsic Locks (Monitors)
Every object in Java inherently has an associated intrinsic lock, also known as a monitor. When a thread enters a synchronized
block or method, it attempts to acquire this lock.
- Acquiring the Lock: If the lock is available, the thread acquires it and proceeds to execute the synchronized code.
- Blocking Other Threads: While one thread holds the lock, any other thread attempting to enter the same synchronized block or method (protected by the same lock object) will be blocked and put into a waiting state until the lock is released.
- Releasing the Lock: The lock is automatically released when the thread exits the synchronized block or method, either normally (after completion) or abnormally (due to an exception).
This mechanism guarantees that only one thread can execute the protected code at any given moment, thus preventing data corruption from concurrent access.
Types of Synchronized Constructs
The synchronized
keyword can be applied in two primary ways:
1. Synchronized Methods
When you declare a method as synchronized
, the entire method body becomes a critical section.
-
Non-Static Methods: For
synchronized
non-static methods, the lock acquired is the instance lock of the object on which the method is called. This means that ifobj1.synchronizedMethod()
is called, the lock is onobj1
. No other thread can call any othersynchronized
non-static method onobj1
until the first thread releases the lock. However, other threads can callsynchronized
methods on different instances (obj2.synchronizedMethod()
).public class Counter { private int count = 0; public synchronized void increment() { count++; // This entire method is synchronized on 'this' object } public synchronized int getCount() { return count; // Also synchronized on 'this' object } }
-
Static Methods: For
synchronized
static methods, the lock acquired is the class lock of the class to which the method belongs. This means that only one thread can execute any staticsynchronized
method of that class at a time, regardless of how many instances exist.public class Logger { private static String log = ""; public static synchronized void writeLog(String message) { log += message + "\n"; // This method is synchronized on Logger.class } }
2. Synchronized Blocks
You can also specify a smaller block of code to be synchronized, which often provides more fine-grained control and can improve concurrency. To do this, you must explicitly provide an object that will serve as the lock.
public class DataProcessor {
private Object lock = new Object(); // A dedicated lock object
private int data = 0;
public void processData() {
// ... some non-critical code ...
synchronized (lock) { // Synchronized block on 'lock' object
data++; // This critical section is protected
}
// ... more non-critical code ...
}
public void retrieveData() {
synchronized (lock) { // This also uses the 'lock' object
System.out.println("Data: " + data);
}
}
}
Key Properties of Synchronized
- Mutual Exclusion: Only one thread can execute a synchronized method or block protected by the same lock at any given time. This is the primary mechanism to prevent race conditions.
- Memory Visibility: Beyond mutual exclusion,
synchronized
also guarantees memory visibility. When a thread releases a monitor, a "happens-before" relationship is established, ensuring that all writes made by that thread before releasing the lock are visible to any other thread that subsequently acquires the same lock. This prevents stale data issues. - Reentrancy: Java's intrinsic locks are reentrant. This means that if a thread has already acquired a lock, it can re-enter any other synchronized block or method that is protected by the same lock without deadlocking itself. For example, if a
synchronized
method calls anothersynchronized
method on the same object, the thread doesn't have to re-acquire the lock; it already holds it.
When to Use Synchronized (Practical Scenarios)
synchronized
is ideal for situations where:
- Shared Mutable State: You have variables or data structures that are accessed and modified by multiple threads (e.g., counters, shared lists, caches).
- Critical Sections: You need to define a specific block of code that must be executed by only one thread at a time to maintain data integrity.
- Simple Thread Safety: For straightforward scenarios where complex concurrency constructs (like
java.util.concurrent.locks.Lock
interface) might be overkill.
Comparison: Synchronized Method vs. Synchronized Block
Feature | Synchronized Method | Synchronized Block |
---|---|---|
Lock Object | this (for non-static), Class.class (for static) |
Any object explicitly specified in synchronized(obj) |
Granularity | Entire method is locked. | Only a specific section of code is locked. |
Flexibility | Less flexible; locks the whole method. | More flexible; allows locking only critical sections. |
Concurrency | May reduce concurrency more if non-critical code is within the method. | Can improve concurrency by locking only necessary parts. |
Readability | Simpler syntax, can be easier to read for full method protection. | Slightly more verbose, but clearly delimits the critical section. |
Potential Considerations and Best Practices
- Performance Overhead:
synchronized
operations involve some overhead due due to lock acquisition and release. Overuse can lead to performance bottlenecks. - Deadlocks: Incorrect use of
synchronized
(e.g., acquiring locks in different orders across multiple threads) can lead to deadlocks, where threads endlessly wait for each other to release a lock. - Starvation: A thread might repeatedly lose the race to acquire a lock, potentially never getting to execute its critical section.
- Granularity: Prefer
synchronized
blocks oversynchronized
methods when only a small portion of the method requires protection. This allows other parts of the method to execute concurrently, improving throughput. - Lock Objects: When using
synchronized
blocks, it's generally good practice to synchronize on a private, final object that is not exposed to other parts of the code. This prevents external code from acquiring your lock and potentially causing unexpected behavior or deadlocks.
By understanding these principles, developers can effectively leverage synchronized
to build robust and thread-safe Java applications.