Java > Concurrency and Multithreading > Synchronization and Locks > Reentrant Locks

ReentrantLock Example

This example demonstrates how to use a ReentrantLock to protect a shared resource from concurrent access. ReentrantLock provides more flexibility than synchronized blocks, allowing for features like fairness and interruptibility.

Code Example

This code defines a Counter class that uses a ReentrantLock to protect its count variable. The increment() method acquires the lock before incrementing the count and releases it in a finally block to ensure the lock is always released, even if an exception occurs. The main method creates two threads that increment the counter concurrently. The `join()` method waits for the threads to finish executing before printing the final count.

import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock(); // Acquire the lock
        try {
            count++;
            System.out.println(Thread.currentThread().getName() + ": Count is: " + count);
        } finally {
            lock.unlock(); // Release the lock in a finally block
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

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

Concepts Behind the Snippet

ReentrantLock: A reentrant lock is a synchronization primitive that allows a thread to acquire the same lock multiple times without blocking. This is in contrast to a basic lock, where a thread would block if it tried to acquire a lock it already held.
Locking and Unlocking: The lock() method acquires the lock, and the unlock() method releases it. It is crucial to release the lock in a finally block to prevent deadlocks in case of exceptions.
Fairness: ReentrantLocks can be configured to be 'fair', meaning threads are granted access to the lock in the order they requested it. This prevents thread starvation but can reduce overall throughput.
Thread Safety: Guarantees that multiple threads can safely access and modify shared data without causing data corruption or race conditions.

Real-Life Use Case

Consider a banking application where multiple threads might try to update the balance of an account simultaneously. A ReentrantLock can be used to ensure that only one thread can access and modify the balance at a time, preventing race conditions and ensuring data integrity.

Best Practices

  • Always release the lock in a finally block: This ensures that the lock is always released, even if an exception occurs.
  • Consider fairness: Use a fair lock if thread starvation is a concern, but be aware that it can reduce overall throughput.
  • Avoid holding locks for long periods: This can reduce concurrency and increase contention.
  • Use tryLock() method for non-blocking lock attempts: Useful when you want to avoid indefinite waiting for a lock.

Interview Tip

Be prepared to explain the differences between ReentrantLock and synchronized blocks. Highlight the flexibility and advanced features of ReentrantLock, such as fairness, interruptibility, and the ability to try to acquire the lock without blocking.

When to Use Reentrant Locks

Use ReentrantLock when you need more control over locking than what synchronized blocks offer. This includes situations where you need fairness, interruptibility, or the ability to try to acquire the lock without blocking. They are particularly useful for complex concurrent scenarios where standard synchronization mechanisms are insufficient.

Memory Footprint

ReentrantLock introduces a slight memory overhead compared to intrinsic locks (synchronized keyword). The object itself has to store the state of the lock, including the owner thread and the hold count. However, this overhead is usually negligible compared to the complexity they help manage.

Alternatives

  • Synchronized Blocks: A simpler and more basic way to achieve synchronization. Suitable for simpler synchronization needs.
  • ReadWriteLock: Allows multiple readers to access a resource concurrently, but only one writer at a time. More efficient when reads are much more frequent than writes.
  • Semaphores: Controls access to a limited number of resources. Useful for managing resource pools.

Pros

  • Flexibility: Offers more control over locking compared to synchronized blocks.
  • Fairness: Can be configured to grant access to threads in the order they requested it.
  • Interruptibility: Allows threads to be interrupted while waiting for the lock.
  • TryLock: Provides the tryLock() method for non-blocking lock attempts.

Cons

  • Complexity: More complex to use than synchronized blocks. Requires manual locking and unlocking.
  • Performance Overhead: Can have a slight performance overhead compared to synchronized blocks in simple cases.
  • Potential for Deadlock: Requires careful handling to avoid deadlocks.

FAQ

  • What is the difference between ReentrantLock and synchronized?

    ReentrantLock provides more flexibility and advanced features compared to synchronized blocks, such as fairness, interruptibility, and the ability to try to acquire the lock without blocking. Synchronized blocks are simpler and more basic, but they lack these advanced features.
  • Why should I release the lock in a finally block?

    Releasing the lock in a finally block ensures that the lock is always released, even if an exception occurs. This prevents deadlocks and ensures that other threads can access the shared resource.
  • What is fairness in ReentrantLock?

    Fairness in ReentrantLock means that threads are granted access to the lock in the order they requested it. This prevents thread starvation but can reduce overall throughput.
  • What is the difference between lock() and tryLock()?

    The lock() method blocks until the lock is acquired. The tryLock() method attempts to acquire the lock immediately and returns true if the lock is acquired or false if it is not. The tryLock(long time, TimeUnit unit) method attempts to acquire the lock and waits for a specified time to acquire it.