Overview of Synchronized
The synchronized keyword in Java is a mechanism for coordinating access to shared resources among multiple threads. It ensures that only one thread can execute a synchronized block or method at any given time, thus preventing race conditions and ensuring thread safety.
Synchronized Code Blocks and Methods
A synchronized block restricts access to a specific portion of code by requiring a thread to acquire a lock before execution. This lock can be any Java object.
synchronized(lockObject) {
// critical section
}
When applied to methods, the behavior of synchronized depends on whether the method is static or instance-based:
- Static Methods: The lock is associated with the class's
Classobject. All threads invoking the method on any instance of the class share the same lock. - Instance Methods: The lock is tied to the specific instance of the class. Threads invoking the method on the same instance will share the same lock, while different instances will have separate locks.
Example: Synchronized Method and Block
public void methodA() {
synchronized(this) {
// Access shared resources
}
}
public synchronized void methodB() {
// Access shared resources
}
Both approaches synchronize on the current instance, but the first explicitly uses a block with this as the lock object.
Practical Examples
Shared Instance Lock
class SyncTask implements Runnable {
private int counter = 0;
@Override
public void run() {
synchronized(this) {
for(int i = 0; i < 3; i++) {
try {
counter++;
System.out.println(Thread.currentThread().getName() + ": " + counter);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SyncTask task = new SyncTask();
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
Output:
Thread-2: 1
Thread-2: 2
Thread-2: 3
Thread-1: 4
Thread-1: 5
Thread-1: 6
Different Instance Locks
class SyncTask implements Runnable {
private int counter = 0;
@Override
public void run() {
synchronized(this) {
for(int i = 0; i < 3; i++) {
try {
counter++;
System.out.println(Thread.currentThread().getName() + ": " + counter);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new SyncTask(), "Thread-1");
Thread t2 = new Thread(new SyncTask(), "Thread-2");
t1.start();
t2.start();
}
}
Output:
Thread-1: 1
Thread-2: 1
Thread-1: 2
Thread-2: 2
Thread-1: 3
Thread-2: 3
Class-Level Lock
class SyncTask implements Runnable {
private static int counter = 0;
@Override
public void run() {
synchronized(SyncTask.class) {
for(int i = 0; i < 3; i++) {
try {
counter++;
System.out.println(Thread.currentThread().getName() + ": " + counter);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new SyncTask(), "Thread-1");
Thread t2 = new Thread(new SyncTask(), "Thread-2");
t1.start();
t2.start();
}
}
Output:
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-2: 4
Thread-2: 5
Thread-2: 6
Lock Release and Internals
Locks acquired via synchronized are automatically released when the execution of the synchronized block or method completes. This is handled through the JVM's monitorenter and monitorexit instructions.
Monitor and Lock Management
- Each object in Java has an associated monitor (or intrinsic lock).
- When a thread enters a synchronized block, it executes
monitorenter, incrementing the monitor's counter. - If another thread attempts to enter, it will be blocked until the lock is released via
monitorexit. - Reentrancy is supported: a thread can reacquire the same lock multiple times without deadlock.
Lock Optimization in JVM
In JDK 1.6+, the JVM introduced optimizations to reduce the overhead of synchronization:
- Biased Locking: Optimized for single-threaded access. The lock is biased towards the first thread that acquires it, avoiding expensive atomic operations for subsequent reacquisitions.
- Lightweight Locking: Uses CAS (Compare and Swap) operations to avoid thread blocking during contention. If CAS fails, the thread spins (busy-waits) to acquire the lock.
- Heavyweight Locking: If contention is high and spinning fails, the JVM uses OS-level thread blocking, which is expensive but necessary for fairness and correctness.
Comparison with Volatile
volatile is a lighter mechenism that ensures visibility of changes across threads but does not provide atomicity or mutual exclusion:
synchronizedensures both visibility and atomicity.volatileonly guarantees visibility and prevents instruction reordering, but not atomic operations.