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

CharacteristicvolatilesynchronizedReentrantLock
VisibilityGuaranteedGuaranteedGuaranteed
MutexNot guaranteedGuaranteedGuaranteed
ReentrancyNot applicableSupportedSupported
ScopeOnly variablesBlocks/methodsBlocks/methods
Lock acquisitionNoneAutomaticExplicit
FairnessNoneNoneSupported
Performance overheadLowModerateHigh

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.

Avatar

By BytePilot

Because sharing makes us better. Let’s learn, build, and grow — one byte at a time.

Leave a Reply

Your email address will not be published. Required fields are marked *