Java > Concurrency and Multithreading > Thread Basics > Thread Synchronization

Using ReentrantLock for Thread Synchronization

This snippet demonstrates thread synchronization using `ReentrantLock` from the `java.util.concurrent.locks` package. `ReentrantLock` provides more control and flexibility compared to synchronized blocks, including fair locking and interruptible waiting.

Code Snippet

This code defines a `ReentrantLockCounter` class with a private `count` variable and a `ReentrantLock` object. The `increment()` and `getCount()` methods use the `lock()` and `unlock()` methods of the `ReentrantLock` to protect the `count` variable from concurrent access. The `try-finally` block ensures that the lock is always released, even if an exception occurs. The `main` method creates two threads that increment the counter, and the final count is printed after both threads have finished executing. `ReentrantLock` ensures only one thread can increment or retrieve the count value at the same time.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockCounter {

    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

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

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

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

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

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

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

Concepts Behind the Snippet

`ReentrantLock` is an implementation of the `Lock` interface that provides reentrant locking semantics. This means that a thread that already holds the lock can acquire it again without blocking. `ReentrantLock` also offers features like fair locking, where threads are granted access to the lock in the order they requested it, and interruptible waiting, where threads can be interrupted while waiting for the lock.

Real-Life Use Case

Consider a resource management system where multiple threads are competing for access to a limited number of resources. `ReentrantLock` can be used to implement a fair locking mechanism, ensuring that threads are granted access to the resources in the order they requested them. This can prevent starvation, where some threads are indefinitely denied access to the resources.

Best Practices

  • Always Use `try-finally` Blocks: Ensure that the `unlock()` method is always called in a `finally` block to release the lock, even if an exception occurs.
  • Consider Fair Locking: If fairness is important, use the fair locking constructor of `ReentrantLock`.
  • Use Interruptible Waiting: If threads may need to be interrupted while waiting for the lock, use the `lockInterruptibly()` method.

Interview Tip

Be prepared to compare and contrast `ReentrantLock` with the `synchronized` keyword. Explain the advantages of `ReentrantLock`, such as fair locking, interruptible waiting, and the ability to acquire and release locks in different scopes. Also be able to discuss the potential performance implications of using `ReentrantLock`.

When to Use Them

Use `ReentrantLock` when you need more control and flexibility than the `synchronized` keyword provides. This includes scenarios where you need fair locking, interruptible waiting, or the ability to acquire and release locks in different scopes. If the `synchronized` keyword meet the needs, use it.

Memory Footprint

The memory footprint of `ReentrantLock` is similar to that of synchronized blocks. It requires a small amount of memory to store the lock's state (e.g., the thread holding the lock, the number of holds). The main overhead comes from the potential blocking of threads waiting to acquire the lock.

Alternatives

Alternatives to `ReentrantLock` include:

  • Synchronized Blocks: A simpler synchronization mechanism that may be sufficient for basic synchronization needs.
  • ReadWriteLock: Allows multiple readers or a single writer to access a shared resource concurrently.
  • Semaphore: Limits the number of threads that can access a shared resource concurrently.
  • CountDownLatch: Allows one or more threads to wait until a set of operations being performed in other threads completes.

Pros

  • More Control: Provides more control and flexibility compared to synchronized blocks.
  • Fair Locking: Supports fair locking, where threads are granted access to the lock in the order they requested it.
  • Interruptible Waiting: Allows threads to be interrupted while waiting for the lock.

Cons

  • More Complex: Requires more code and careful handling compared to synchronized blocks.
  • Potential for Deadlock: Improper use can lead to deadlocks.
  • Performance Overhead: Can introduce performance overhead due to the more complex locking mechanism.

FAQ

  • What is fair locking?

    Fair locking is a locking mechanism where threads are granted access to the lock in the order they requested it. This prevents starvation, where some threads are indefinitely denied access to the lock.
  • What is interruptible waiting?

    Interruptible waiting is a locking mechanism where threads can be interrupted while waiting for the lock. This allows threads to respond to external events or cancel their waiting if necessary.
  • Why is it important to use a `try-finally` block with `ReentrantLock`?

    The `try-finally` block ensures that the `unlock()` method is always called to release the lock, even if an exception occurs. This prevents the lock from being held indefinitely, which could lead to deadlocks and other problems.