oracle/graal · error · IllegalMonitorStateException

Cannot signal lock that is not held

Error message

Cannot signal lock that is not held

What it means

EspressoLock.signal() (the guest-side Condition.signal / Object.notify path) verifies the lock is held by the current thread before signalling waiters; if not, it throws IllegalMonitorStateException with the message 'Cannot signal lock that is not held'.

Source

Thrown at espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/blocking/EspressoLock.java:369

        enterWaitInterruptible(interruptible);
        return interruptible.getResult();
    }

    @Override
    public boolean awaitUntil(Date deadline) throws GuestInterruptedException {
        if (!isHeldByCurrentThread()) {
            throw new IllegalMonitorStateException();
        }
        WaitUntilInterruptible interruptible = new WaitUntilInterruptible(deadline);
        enterWaitInterruptible(interruptible);
        return interruptible.getResult();
    }

    @Override
    @TruffleBoundary
    public void signal() {
        if (!isHeldByCurrentThread()) {
            throw new IllegalMonitorStateException("Cannot signal lock that is not held");
        }
        ensureWaitLockInitialized();
        waitLock.lock();
        try {
            signals = Math.min(signals + 1, waiters);
            waitCondition.signal();
        } finally {
            waitLock.unlock();
        }
    }

    @Override
    @TruffleBoundary
    public void signalAll() {
        if (!isHeldByCurrentThread()) {
            throw new IllegalMonitorStateException("Cannot signal lock that is not held");
        }
        ensureWaitLockInitialized();

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Acquire the lock (synchronized block or lock.lock()) before signal/notify.
  2. Keep notify/signal inside the same critical section that mutates the waited-upon state.
  3. Use @TruffleBoundary-aware host code paths rather than ad-hoc signalling from arbitrary threads.

Example fix

// before
sharedState = value;
obj.notify();

// after
 synchronized (obj) {
     sharedState = value;
     obj.notify();
 }
Defensive patterns

Strategy: validation

Validate before calling

if (lock.isHeldByCurrentThread()) {
    lock.signal();
}

Try / catch

synchronized (obj) {
    try { obj.notify(); }
    catch (IllegalMonitorStateException e) { throw e; /* scope bug */ }
}

Prevention

When it happens

Trigger: Calling signal()/notify() without holding the corresponding EspressoLock - e.g. notify() outside synchronized, or signal() after lock release.

Common situations: Producer/consumer code where notify is in a different method than the synchronized block; refactoring that split lock scope; callback invoked from another thread that never acquired the lock.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/f380e76b95076569. Report an issue: GitHub.