In Java concurrent programming, volatile, synchronized, and ReentrantLock are three commonly used synchronization mechanisms. Each mechanism has its unique characteristics, advantages, disadvantages, and applicable scenarios. Understanding the differences between them and when to use each is crucial for improving the performance and reliability of programs. This article will explore the characteristics, usage scenarios, and examples of these three mechanisms in detail.
1. Characteristics of volatile
1.1 Ensures Visibility
A variable modified by volatile ensures that all threads can see the latest value of the variable, avoiding cache inconsistency issues between threads.
Example
public class VolatileVisibility {
private volatile boolean running = true;
public void stop() {
running = false; // Modify running
}
public void execute() {
while (running) {
// Execute task
}
}
}
In this example, the running variable is declared as volatile. When one thread calls the stop() method, other threads will immediately see that running is false.
1.2 Prohibits Instruction Reordering
volatile also ensures that operations on the variable will not be reordered, thus guaranteeing the sequentiality of program execution.
Example
public class VolatileReordering {
private int a = 0;
private volatile int b = 0;
public void method1() {
a = 1; // 1
b = 2; // 2
}
public void method2() {
if (b == 2) { // 3
System.out.println(a); // 4
}
}
}
Without volatile, reordering might occur, causing method2() to execute before b = 2 in method1(), resulting in a possibly being 0.
1.3 Does Not Guarantee Atomicity
volatile cannot guarantee the atomicity of compound operations, such as increment operations.
Example
public class VolatileAtomicity {
private volatile int count = 0;
public void increment() {
count++; // Non-atomic operation
}
public int getCount() {
return count;
}
}
In this example, the increment of count in the increment() method is not an atomic operation, which may lead to data inconsistency.
2. Characteristics of synchronized
2.1 Reentrancy
synchronized allows the same thread to acquire the same lock multiple times without causing a deadlock.
Example
public class SynchronizedReentrancy {
public synchronized void method1() {
method2(); // Reentrancy is allowed
}
public synchronized void method2() {
// Execute task
}
}
In this example, method1() can safely call method2() because synchronized supports reentrancy.
2.2 Non-interruptibility
The acquisition of a synchronized lock is non-interruptible; a thread cannot be interrupted while waiting for the lock.
Example
public class SynchronizedInterruptibility {
public synchronized void lockedMethod() throws InterruptedException {
Thread.sleep(10000); // Simulate long execution
}
public void execute() {
Thread thread = new Thread(() -> {
try {
lockedMethod();
} catch (InterruptedException e) {
// Handle interruption
}
});
thread.start();
thread.interrupt(); // Thread is interrupted while waiting for the lock
}
}
In this example, lockedMethod() cannot be interrupted, causing the thread to fail to release the lock.
2.3 Lock Upgrade and Downgrade
synchronized supports lock upgrade and downgrade. It can be used directly in methods or within code blocks.
Example
public class SynchronizedUpgrade {
public synchronized void method() {
// Hold object lock
synchronized (this) {
// Hold the same lock
}
}
}
In this example, object locks and class locks are used, demonstrating lock upgrade and downgrade.
2.4 Unfairness
synchronized does not guarantee fairness, which may cause some threads to wait for a long time.
Example
public class SynchronizedFairness {
public synchronized void method() {
// Execute task
}
}
In this example, when multiple threads access method(), there is no guarantee that the thread that requested the lock first will acquire it first.
2.5 Visibility, Atomicity, and Ordering
synchronized guarantees visibility, atomicity, and ordering of shared variables.
Example
public class SynchronizedVisibility {
private int data;
public synchronized void updateData(int value) {
data = value; // Update data
}
public synchronized int readData() {
return data; // Read data
}
}
In this example, the updateData() and readData() methods ensure the thread safety of data.
3. Characteristics of ReentrantLock
3.1 Reentrancy
ReentrantLock allows the same thread to acquire the lock multiple times, supporting reentrancy.
Example
public class ReentrantLockReentrancy {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
method(); // Reentrancy is allowed
} finally {
lock.unlock();
}
}
}
In this example, the method() can safely call itself because ReentrantLock supports reentrancy.
3.2 Interruptibility
ReentrantLock allows interruption while waiting for the lock, providing better control.
Example
public class ReentrantLockInterruptibility {
private final ReentrantLock lock = new ReentrantLock();
public void lockedMethod() throws InterruptedException {
lock.lockInterruptibly(); // Interruptible lock
try {
// Execute task
} finally {
lock.unlock();
}
}
}
In this example, lockedMethod() can be interrupted, offering better control.
3.3 Fairness and Unfairness
ReentrantLock supports the selection of fair or unfair locks.
Example
ReentrantLock fairLock = new ReentrantLock(true); // Fair lock
ReentrantLock unfairLock = new ReentrantLock(); // Unfair lock
In this example, the fair lock acquires the lock in the order of thread requests, while the unfair lock may cause thread starvation.
3.4 Condition Variables
ReentrantLock provides support for condition variables, enabling complex inter-thread collaboration.
Example
public class ReentrantLockCondition {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
condition.await(); // Wait for condition
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal(); // Wake up waiting threads
} finally {
lock.unlock();
}
}
}
In this example, condition variables are used to implement inter-thread collaboration.
4. Differences Between the Three
Characteristic | volatile | synchronized | ReentrantLock |
Visibility | Guaranteed | Guaranteed | Guaranteed |
Mutex | Not guaranteed | Guaranteed | Guaranteed |
Reentrancy | Not applicable | Supported | Supported |
Scope | Only variables | Blocks/methods | Blocks/methods |
Lock acquisition | None | Automatic | Explicit |
Fairness | None | None | Supported |
Performance overhead | Low | Moderate | High |
5. Analysis of Applicable Scenarios
5.1 When to Use volatile
Scenario: Use volatile when you need to ensure the visibility of a variable but do not require mutually exclusive access.
Example
public class VolatileFlag {
private volatile boolean flag = true;
public void stop() {
flag = false;
}
public void run() {
while (flag) {
// Execute task
}
}
}
In this scenario, volatile can effectively reduce context switching and improve performance.
5.2 When to Use synchronized
Scenario: Use synchronized when you need mutually exclusive access to shared resources.
Example
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
In this example, synchronized ensures the thread safety of count.
5.3 When to Use ReentrantLock
Scenario: Use ReentrantLock when you need more flexible locking mechanisms, such as reentrancy, fairness, or interruptible locks.
Example
public class ReentrantLockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
In this example, ReentrantLock allows flexible control over lock acquisition and release.
6. Summary
Through the in-depth analysis of volatile, synchronized, and ReentrantLock in this article, readers can understand their respective characteristics, advantages, disadvantages, and applicable scenarios. In concurrent programming, choosing the appropriate synchronization mechanism can not only improve program performance but also effectively avoid potential thread safety issues.