ReentrantLock in Java with Example

ReentrantLock in Java is part of the java.util.concurrent.locks package and provides a mechanism for thread synchronization similar to synchronized blocks/methods, but with more advanced features and capabilities. A ReentrantLock is so named because it allows threads to enter the lock on a resource more than once — hence, it’s “reentrant”.

Key Features of ReentrantLock

  1. Reentrant: A thread can hold the same lock multiple times. The lock keeps a count of the number of holds on it. When the count reaches zero, the lock is released.
  2. Interruptible Lock Acquisitions: Threads trying to acquire a lock can be interrupted. This is useful for preventing deadlock situations.
  3. Fairness: Optionally, the lock can be fair, meaning it grants access in a first-come, first-served order, if constructed with the fair flag set to true.
  4. Lock Polling and Timed Locks: Provides methods to acquire a lock with a timeout (tryLock()), allowing threads to wait for a lock for a certain period before giving up.
  5. Condition Support: Supports Condition objects, which allow threads to wait for certain conditions to be met while the lock is held, similar to the Object.wait() and notify() methods in synchronized blocks.

Basic Usage Example

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();  // Acquire the lock
        try {
            count++;
        } finally {
            lock.unlock();  // Always ensure to unlock in finally block
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // Example usage in threads
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Count: " + counter.getCount());
    }
}

In this example, ReentrantLock is used to ensure that only one thread can execute the increment method at a time, thus avoiding concurrent modification issues.

When to Use ReentrantLock

  • Advanced Synchronization: Use ReentrantLock when you need advanced synchronization features that are not available with synchronized blocks/methods.
  • Fine-Grained Control: It offers more control, such as the ability to try to acquire a lock without waiting forever, and to interrupt a thread waiting to acquire a lock.
  • Fairness Policy: When you need a fairness policy for thread scheduling.

Conclusion

ReentrantLock is a powerful tool for controlling access to shared resources in multi-threaded environments. However, it’s more complex than synchronized blocks and should be used when its advanced features are required. Always remember to unlock in a finally block to ensure that locks are released even if an exception occurs.