Home ยป Reentrant Monitors in Java tutorial with code examples

Reentrant Monitors in Java tutorial with code examples

In Java, monitors (also known as intrinsic locks) are used to control access to critical sections of code that should not be executed by multiple threads simultaneously. These monitors ensure mutual exclusion and thread synchronization.

A Reentrant Monitor allows a thread that has already acquired a lock to re-enter the same synchronized block or method it previously entered.

This is essential when a thread needs to acquire the same lock multiple times, such as when it calls methods within synchronized methods or re-enters synchronized blocks.

In Java, the concept of reentrant monitors is built into the synchronized keyword. Additionally, Java provides a more flexible class, ReentrantLock, from the java.util.concurrent.locks package, which allows explicit lock acquisition and release with advanced features like fairness policies and interruptibility.

1. Using synchronized as a Reentrant Monitor

In Java, every object has an intrinsic lock, and when a thread enters a synchronized method or block, it acquires the lock. If the same thread attempts to enter another synchronized block that requires the same lock, it can re-enter (hence “reentrant”) without deadlocking.

Example 1: Reentrant Synchronization with synchronized

class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("Inside method A");
        methodB(); // Re-entrant lock: This thread will not block here.
    }

    public synchronized void methodB() {
        System.out.println("Inside method B");
    }
}

public class ReentrantMonitorExample1 {
    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.methodA();
    }
}

Output:

Inside method A
Inside method B

Explanation:

The method methodA() is synchronized, meaning it requires the current thread to acquire the objectโ€™s intrinsic lock.
When methodB() is called within methodA(), the thread can re-enter the lock it already holds, demonstrating reentrant behavior.

2. ReentrantLock: Advanced Reentrant Monitor

The ReentrantLock class is part of the java.util.concurrent.locks package and provides additional features compared to the synchronized keyword:

You can explicitly lock and unlock.
It supports interruptible and timed lock acquisition.
It allows fairness policies (to ensure locks are granted in order of request).

Example 2: Using ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockExample {
    private ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock(); // Acquire the lock
        try {
            System.out.println("Inside method A");
            methodB(); // Re-entrant lock: This thread can acquire the lock again.
        } finally {
            lock.unlock(); // Release the lock
        }
    }

    public void methodB() {
        lock.lock(); // Acquire the lock
        try {
            System.out.println("Inside method B");
        } finally {
            lock.unlock(); // Release the lock
        }
    }
}

public class ReentrantMonitorExample2 {
    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.methodA();
    }
}

Output:

Inside method A
Inside method B

Explanation:

The ReentrantLock allows the same thread to re-enter the lock when it calls methodB() from within methodA().
The lock.lock() method acquires the lock, and lock.unlock() releases it.
Reentrant locks ensure that a thread that has already acquired the lock can reacquire it.

3. Preventing Deadlock with Reentrant Monitors

Without reentrant locks, recursive calls to synchronized methods could lead to deadlock.

Hereโ€™s an example where the absence of reentrancy would cause deadlock, but reentrant monitors allow the thread to proceed safely.

Example 3: Recursive Reentrant Lock

class ReentrantRecursiveExample {
    public synchronized void recursiveMethod(int count) {
        if (count > 0) {
            System.out.println("Recursive count: " + count);
            recursiveMethod(count - 1); // Re-enter the same lock
        }
    }
}

public class ReentrantMonitorExample3 {
    public static void main(String[] args) {
        ReentrantRecursiveExample example = new ReentrantRecursiveExample();
        example.recursiveMethod(5); // Recursively call the method with a countdown
    }
}

Output:

Recursive count: 5
Recursive count: 4
Recursive count: 3
Recursive count: 2
Recursive count: 1

Explanation:

The recursiveMethod() calls itself recursively, and since it's synchronized, the current thread re-enters the same lock multiple times.
Without a reentrant monitor, this would result in a deadlock. However, with reentrancy, the same thread can reacquire the lock as needed.

4. Fairness with ReentrantLock

The ReentrantLock class supports a fairness policy. When fairness is enabled, locks are granted in the order they were requested, preventing “starvation” of threads.

Example 4: Using Fair ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

class FairReentrantLockExample {
    private ReentrantLock lock = new ReentrantLock(true); // Fair lock

    public void accessResource(String threadName) {
        lock.lock(); // Acquire the lock
        try {
            System.out.println(threadName + " has acquired the lock.");
            Thread.sleep(1000); // Simulate some work
        } catch (InterruptedException e) {
            System.out.println(e);
        } finally {
            System.out.println(threadName + " is releasing the lock.");
            lock.unlock(); // Release the lock
        }
    }
}

public class ReentrantMonitorExample4 {
    public static void main(String[] args) {
        FairReentrantLockExample example = new FairReentrantLockExample();
        Runnable task = () -> example.accessResource(Thread.currentThread().getName());

        // Start multiple threads
        Thread t1 = new Thread(task, "Thread 1");
        Thread t2 = new Thread(task, "Thread 2");
        Thread t3 = new Thread(task, "Thread 3");

        t1.start();
        t2.start();
        t3.start();
    }
}

Output:

Thread 1 has acquired the lock.
Thread 1 is releasing the lock.
Thread 2 has acquired the lock.
Thread 2 is releasing the lock.
Thread 3 has acquired the lock.
Thread 3 is releasing the lock.

Explanation:

The ReentrantLock is created with the fairness flag set to true, which ensures that threads acquire the lock in the order they requested it.
This prevents thread starvation, where one thread might monopolize the lock while other threads are waiting.

5. Try Locking with ReentrantLock

The ReentrantLock class provides a tryLock() method, which allows a thread to attempt to acquire the lock without blocking indefinitely. This is useful when you don't want a thread to wait forever for a lock.

Example 5: Using tryLock()

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

class TryLockExample {
    private ReentrantLock lock = new ReentrantLock();

    public void accessResource(String threadName) {
        try {
            if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { // Try to acquire the lock for 500ms
                try {
                    System.out.println(threadName + " has acquired the lock.");
                    Thread.sleep(1000); // Simulate some work
                } finally {
                    System.out.println(threadName + " is releasing the lock.");
                    lock.unlock(); // Release the lock
                }
            } else {
                System.out.println(threadName + " could not acquire the lock.");
            }
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}

public class ReentrantMonitorExample5 {
    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();
        Runnable task = () -> example.accessResource(Thread.currentThread().getName());

        // Start multiple threads
        Thread t1 = new Thread(task, "Thread 1");
        Thread t2 = new Thread(task, "Thread 2");

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

Output:

Thread 1 has acquired the lock.
Thread 2 could not acquire the lock.
Thread 1 is releasing the lock.

Explanation:

tryLock() allows the thread to attempt to acquire the lock without blocking indefinitely.
If the lock is not available within 500 milliseconds, the thread proceeds without acquiring the lock.

6. Using ReentrantLock with Conditions

The ReentrantLock class supports conditions via the Condition object. This allows threads to wait for specific conditions before proceeding, similar to Object.wait() and Object.notify().

Example 6: Using Condition with ReentrantLock

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

class ConditionExample {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean isConditionMet = false;

    public void waitForCondition() throws InterruptedException {
        lock.lock();
        try {
            while (!isConditionMet) {
                System.out.println("Waiting for the condition to be met...");
                condition.await(); // Wait for the condition
            }
            System.out.println("Condition met, proceeding...");
        } finally {
            lock.unlock();
        }
    }

    public void signalCondition() {
        lock.lock();
        try {
            isConditionMet = true;
            condition.signalAll(); // Signal all waiting threads
        } finally {
            lock.unlock();
        }
    }
}

public class ReentrantMonitorExample6 {
    public static void main(String[] args) {
        ConditionExample example = new ConditionExample();

        // Thread waiting for the condition
        Thread waiter = new Thread(() -> {
            try {
                example.waitForCondition();
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        });

        // Thread signaling the condition
        Thread signaler = new Thread(example::signalCondition);

        waiter.start();

        try {
            Thread.sleep(2000); // Simulate some delay before signaling
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        signaler.start();
    }
}

Output:

Waiting for the condition to be met...
Condition met, proceeding...

Explanation:

The Condition object is used to make the waitForCondition() thread wait until a specific condition is met.
The signaler thread calls signalCondition(), which changes the condition and wakes up the waiting thread.

Conclusion

In Java, reentrant monitors are an essential mechanism for controlling access to shared resources across multiple threads.

The built-in synchronized keyword provides basic reentrant locking functionality, while the ReentrantLock class from the java.util.concurrent.locks package offers more flexibility and features such as fairness policies, timed locking, and explicit lock handling.

Key points:

Reentrancy: A thread holding a lock can acquire it again (useful for recursive and nested lock acquisitions).
synchronized keyword: Automatically provides reentrancy, though it's implicit and has no extra features.
ReentrantLock: Provides explicit locking with features like fairness, timed locks, and conditions.
tryLock(): Useful for attempting to acquire a lock without blocking indefinitely.
Condition: Allows fine-grained control over thread communication and synchronization.

You may also like