adllm Insights logo adllm Insights logo

Debugging IllegalMonitorStateException: ReentrantLock with wait/notify vs. Condition

Published on by The adllm Team. Last modified: . Tags: Java Concurrency Multithreading ReentrantLock Condition IllegalMonitorStateException wait notify Debugging

Java’s IllegalMonitorStateException is a common yet often misunderstood runtime exception in concurrent programming. It typically signals a fundamental mismatch between a thread’s attempt to use an object’s intrinsic monitor methods (wait(), notify(), notifyAll()) and its actual ownership of that monitor. This issue frequently surfaces when developers start using explicit locks like java.util.concurrent.locks.ReentrantLock but incorrectly attempt to pair them with the Object class’s monitor methods.

This article provides a comprehensive guide to understanding why this specific IllegalMonitorStateException occurs with ReentrantLock and Object.wait()/notify(), how to diagnose it, and crucially, how to correctly use ReentrantLock with its associated Condition objects for proper inter-thread communication.

Understanding the Core Problem: Intrinsic vs. Explicit Locks

At the heart of this issue lies the distinction between Java’s two primary locking mechanisms: intrinsic locks (monitors) and explicit locks provided by the java.util.concurrent.locks package.

Intrinsic Locks and Object.wait(), notify(), notifyAll()

Every Java object has an associated intrinsic lock, also known as a monitor.

  • The synchronized keyword in Java is used to acquire and release an object’s intrinsic lock. When a thread enters a synchronized block or method, it attempts to acquire the lock on the specified object.
  • The methods wait(), notify(), and notifyAll() are defined in the Object class and are designed to work exclusively with these intrinsic locks.
  • Crucially, a thread MUST own the intrinsic lock (monitor) of an object before it can call wait(), notify(), or notifyAll() on that object. If it doesn’t, an IllegalMonitorStateException is thrown.

Explicit Locks: ReentrantLock and Condition

ReentrantLock is a more flexible and powerful alternative to synchronized blocks.

  • It is an explicit lock, meaning you must manually acquire (lock()) and release (unlock()) it.
  • ReentrantLock does not use the object’s intrinsic monitor system. It manages its own locking state.
  • For inter-thread communication (waiting for a condition and signaling other threads), ReentrantLock provides Condition objects. A Condition instance is obtained by calling lock.newCondition().
  • Condition objects offer their own versions of wait/notify mechanisms: await(), signal(), and signalAll().

The Mismatch Leading to IllegalMonitorStateException

The IllegalMonitorStateException in the context of ReentrantLock arises when a thread:

  1. Acquires a ReentrantLock.
  2. Then, attempts to call wait(), notify(), or notifyAll() on some object (say, sharedObject.wait()).

Even if the thread holds the ReentrantLock, it does not automatically own the intrinsic monitor of sharedObject (unless it also happens to be in a synchronized(sharedObject) block, which would be unusual and confusing). Since Object.wait() requires ownership of sharedObject’s intrinsic monitor, and the thread doesn’t have it (it only has the ReentrantLock), the JVM throws IllegalMonitorStateException.

The Wrong Approach: Illustrating the Pitfall

Let’s look at a common incorrect attempt to use ReentrantLock with Object.wait() and Object.notifyAll().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class IncorrectResourceAccess {
    private final Lock lock = new ReentrantLock();
    // Using 'this' for wait/notify, or any other shared object
    private boolean resourceAvailable = false;

    public void produceResource() {
        lock.lock(); // Acquires the ReentrantLock
        try {
            System.out.println(Thread.currentThread().getName() + 
                               " acquired lock, producing resource...");
            resourceAvailable = true;
            // MISTAKE: Calling notifyAll() on 'this' object's intrinsic monitor
            // while only holding the ReentrantLock.
            // This requires synchronized(this) block.
            synchronized (this) { // This "fixes" it but isn't ReentrantLock way
                 this.notifyAll(); 
            }
            // If the synchronized block above is removed, the line below throws:
            // this.notifyAll(); // Likely throws IllegalMonitorStateException
            System.out.println(Thread.currentThread().getName() + 
                               " notified consumers.");
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + 
                               " released lock.");
        }
    }

    public void consumeResource() throws InterruptedException {
        lock.lock(); // Acquires the ReentrantLock
        try {
            System.out.println(Thread.currentThread().getName() + 
                               " acquired lock, attempting to consume.");
            // MISTAKE: Calling wait() on 'this' object's intrinsic monitor
            // while only holding the ReentrantLock.
            // This requires synchronized(this) block.
            while (!resourceAvailable) {
                System.out.println(Thread.currentThread().getName() + 
                                   " resource not available, waiting...");
                // If not in a synchronized(this) block, this.wait() throws:
                // this.wait(); // Likely throws IllegalMonitorStateException
                
                // For demonstration, assume one might forget and do this:
                // If the next line is used WITHOUT synchronized(this), it throws.
                // To make it work *with* intrinsic locks, you'd need:
                synchronized (this) { // This "fixes" it but mixes mechanisms
                    if (!resourceAvailable) this.wait(1000); 
                }
            }
            if (resourceAvailable) {
                resourceAvailable = false;
                System.out.println(Thread.currentThread().getName() + 
                                   " consumed resource.");
            } else {
                System.out.println(Thread.currentThread().getName() + 
                                   " timed out waiting for resource.");
            }
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + 
                               " released lock.");
        }
    }

    public static void main(String[] args) {
        IncorrectResourceAccess resource = new IncorrectResourceAccess();
        // To truly test the exception, modify produce/consume to directly call
        // this.wait()/notifyAll() outside a synchronized(this) block
        // when the lock variable is held.
        // The code above is structured to run if synchronized(this) is used,
        // but highlights where the error would occur if not.
        System.out.println("Demonstration assumes a scenario where the " +
                           "direct calls");
        System.out.println("to wait()/notifyAll() are made without " +
                           "synchronized(this)");
        System.out.println("while ReentrantLock is held.");
    }
}

In the example above, if this.wait() or this.notifyAll() were called directly (without the corrective synchronized(this) blocks shown for illustrative purposes), while lock (the ReentrantLock) is held, an IllegalMonitorStateException would occur. This is because holding lock does not grant the thread ownership of the intrinsic monitor of the IncorrectResourceAccess instance (this).

The Correct Approach: Using Condition Objects

The correct way to achieve wait/notify semantics with ReentrantLock is to use Condition objects.

  1. Create Condition Instances: Obtain one or more Condition instances from your ReentrantLock using lock.newCondition().
  2. Use await(), signal(), signalAll():
    • Replace object.wait() calls with condition.await().
    • Replace object.notify() calls with condition.signal().
    • Replace object.notifyAll() calls with condition.signalAll().
  3. Hold the Lock: The ReentrantLock must be held by the current thread when calling any of these Condition methods. If not, the Condition methods themselves will throw an IllegalMonitorStateException.
  4. try-finally Block: Always release the ReentrantLock in a finally block to ensure it’s unlocked even if exceptions occur.
  5. Wait in a Loop: Always use await() within a while loop that checks the condition predicate to guard against spurious wakeups (see note on wait() Javadoc).

Here’s the corrected version of the previous example using ReentrantLock and Condition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CorrectResourceAccess {
    private final Lock lock = new ReentrantLock();
    // Condition variable associated with the lock
    private final Condition resourceCondition = lock.newCondition();
    private boolean resourceAvailable = false;

    public void produceResource() {
        lock.lock(); // Acquire the ReentrantLock
        try {
            System.out.println(Thread.currentThread().getName() + 
                               " acquired lock, producing resource...");
            resourceAvailable = true;
            System.out.println(Thread.currentThread().getName() + 
                               " resource produced, signaling consumers.");
            // Signal waiting threads using the Condition object
            resourceCondition.signalAll();
        } finally {
            lock.unlock(); // Release the lock in a finally block
            System.out.println(Thread.currentThread().getName() + 
                               " released lock.");
        }
    }

    public void consumeResource() throws InterruptedException {
        lock.lock(); // Acquire the ReentrantLock
        try {
            System.out.println(Thread.currentThread().getName() + 
                               " acquired lock, attempting to consume.");
            // Wait for the resource to become available in a loop
            while (!resourceAvailable) {
                System.out.println(Thread.currentThread().getName() + 
                                   " resource not available, awaiting...");
                // Wait on the Condition object; this atomically releases 'lock'
                // and reacquires it before await() returns.
                resourceCondition.await(); 
            }
            // At this point, the lock is held and resourceAvailable is true
            resourceAvailable = false;
            System.out.println(Thread.currentThread().getName() + 
                               " consumed resource.");
        } finally {
            lock.unlock(); // Release the lock in a finally block
            System.out.println(Thread.currentThread().getName() + 
                               " released lock.");
        }
    }

    public static void main(String[] args) {
        CorrectResourceAccess resource = new CorrectResourceAccess();

        Thread producer = new Thread(() -> resource.produceResource(), "Producer");
        Thread consumer1 = new Thread(() -> {
            try {
                resource.consumeResource();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Consumer-1");
        Thread consumer2 = new Thread(() -> {
            try {
                resource.consumeResource();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Consumer-2");

        consumer1.start();
        consumer2.start();
        
        try {
            Thread.sleep(100); // Give consumers a chance to start and wait
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        producer.start();

        try {
            producer.join();
            consumer1.join();
            consumer2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Main thread finished.");
    }
}

In this corrected version:

  • resourceCondition.await() is used instead of this.wait().
  • resourceCondition.signalAll() is used instead of this.notifyAll().
  • These Condition methods are correctly called while the lock (the ReentrantLock instance) is held by the current thread.

Key Differences Summarized

Featuresynchronized / Object.wait/notifyReentrantLock / Condition.await/signal
Lock TypeIntrinsic (monitor)Explicit Lock implementation
AcquisitionImplicit (via synchronized keyword)Explicit (lock(), tryLock())
ReleaseImplicit (exiting synchronized block/method)Explicit (unlock(), must be in finally)
Wait/Notify APIObject.wait(), notify(), notifyAll()Condition.await(), signal(), signalAll()
Monitor SourceSingle intrinsic monitor per objectMultiple Condition objects per Lock possible
FlexibilityBasic, good for most casesMore features (timed waits, interruptible lock acquisition, fairness, multiple conditions)
Causes IllegalMonitorStateExceptionCalling wait/notify without owning the object’s intrinsic monitor.Calling await/signal without owning the associated Lock. OR calling Object.wait/notify when only the ReentrantLock is held.

Debugging Strategies for IllegalMonitorStateException

When you encounter an IllegalMonitorStateException:

  1. Analyze the Stack Trace: The exception message “current thread is not owner” is a strong indicator. Note the exact line where it occurs.

  2. Code Review Checklist:

    • Identify the object: On which object’s monitor was wait(), notify(), or notifyAll() called?
    • Check for synchronized: Is the problematic call enclosed within a synchronized block or method that locks on that specific object?
    • Check for ReentrantLock usage: Is ReentrantLock being used for synchronization in this section of code?
      • If yes, ensure you are using Condition.await(), signal(), or signalAll() from a Condition object associated with that ReentrantLock.
      • Verify that the ReentrantLock is indeed held by the current thread when these Condition methods are called (e.g., lock.lock() was called before and lock.unlock() hasn’t been called yet for that acquisition). You can check this with isHeldByCurrentThread().
    • Verify unlock() calls: Ensure every lock.lock() is paired with a lock.unlock() in a finally block. Also, ensure unlock() is not called by a thread that doesn’t hold the lock, or more times than lock() was called.
  3. Using a Debugger:

    • Set a breakpoint just before the line that throws the exception.
    • Inspect the current thread.
    • If ReentrantLock is involved, check lock.isHeldByCurrentThread(). It should be true if you intend to call Condition methods.
    • If Object.wait/notify is called, verify if the thread actually owns the intrinsic monitor of the target object. Most IDE debuggers can show monitor information for threads and objects.

Common Pitfalls & Best Practices with ReentrantLock and Condition

  • Forgetting unlock() in finally: This is a classic error that can lead to deadlocks or resource starvation as the lock is never released if an exception occurs in the try block.
  • Calling Condition methods without holding the lock: await(), signal(), or signalAll() will throw IllegalMonitorStateException if the associated ReentrantLock is not held by the current thread.
  • Using if instead of while for await():
    1
    2
    3
    4
    5
    6
    7
    8
    
    // Incorrect:
    // if (!conditionPredicate) {
    //     condition.await();
    // }
    // Correct:
    while (!conditionPredicate) {
        condition.await(); // Handles spurious wakeups
    }
    
    A thread can wake up from await() for reasons other than a specific signal() (spurious wakeups). The condition predicate must always be re-checked in a loop.
  • Lost Signals: If condition.signal() is called before any thread calls condition.await(), the signal is lost. Threads subsequently calling await() will block indefinitely if no further signals occur.
  • signal() vs. signalAll():
    • signal(): Wakes up one arbitrarily chosen waiting thread. Use if any waiting thread can make progress.
    • signalAll(): Wakes up all waiting threads. Use if multiple threads might be waiting for conditions that have now become true, or if you’re unsure. Be aware this can lead to a “thundering herd” problem if many threads wake up only to find they cannot proceed and go back to waiting, though ReentrantLock handles this better than raw synchronized blocks might in some cases.

Advanced ReentrantLock and Condition Features

ReentrantLock and Condition offer more than basic locking and waiting:

  • Fairness Policy: ReentrantLock can be constructed with a fairness policy (new ReentrantLock(true)). Fair locks grant access to the longest-waiting thread, preventing starvation but potentially at a cost to overall throughput.
  • Timed and Interruptible Waits: Condition.await() is interruptible by default. There are also timed versions like await(long time, TimeUnit unit), awaitNanos(long nanosTimeout), and an uninterruptible version awaitUninterruptibly().
  • Multiple Condition Variables: A single ReentrantLock can be associated with multiple Condition objects. This is extremely useful for managing different wait-sets for different state conditions related to the same locked resource (e.g., notFull and notEmpty conditions in a bounded buffer).

Conclusion

The IllegalMonitorStateException encountered when using ReentrantLock often stems from a misunderstanding of how its inter-thread communication mechanism (Condition objects) differs from the intrinsic monitor methods (Object.wait(), notify(), notifyAll()) associated with synchronized blocks.

By recognizing that ReentrantLock requires the explicit use of Condition.await(), Condition.signal(), and Condition.signalAll()—and that these methods must be called while holding the ReentrantLock—developers can avoid this common pitfall. Adhering to best practices, such as releasing locks in finally blocks and checking wait conditions in loops, will lead to more robust, correct, and maintainable concurrent Java applications.