alibaba/nacos · error · IllegalMonitorStateException

Recursive unlock() detected for key={}

Error message

Recursive unlock() detected for key={}

What it means

Thrown by NacosLock.unlock() when the inUnlock ThreadLocal is already TRUE, meaning unlock() is being called recursively from within the same unlock() invocation — e.g. via a callback, signal handler, or shutdown hook triggered during the unlock flow. This guard prevents read-modify-write corruption of localReentrantCount. The exception type is IllegalMonitorStateException.

Source

Thrown at client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java:290

                firstAttempt = false;
                grpcClient.waitForNotification(key, currentOwner(), remaining);
            } catch (InterruptedException e) {
                grpcClient.cancelWait(key, lockType, currentOwner());
                localReentrantCount.remove();
                throw e;
            } catch (NacosException e) {
                grpcClient.cancelWait(key, lockType, currentOwner());
                localReentrantCount.remove();
                LOGGER.error("Failed to try lock with timeout, key={}", key, e);
                return false;
            }
        }
    }
    
    @Override
    public void unlock() {
        if (inUnlock.get()) {
            throw new IllegalMonitorStateException("Recursive unlock() detected for key=" + key);
        }
        inUnlock.set(Boolean.TRUE);
        boolean removed = false;
        try {
            int count = localReentrantCount.get();
            if (count <= 0) {
                throw new IllegalMonitorStateException("Current thread does not hold the lock");
            }
            try {
                LockInstance instance = buildInstance(0);
                LockResult result = grpcClient.unLockWithResult(instance);
                if (result.isSuccess()) {
                    localReentrantCount.set(count - 1);
                    if (result.getReentrantCount() == 0) {
                        watchdog.unregister(key);
                        localReentrantCount.remove();
                        removed = true;
                    }

View on GitHub (pinned to 9b989acdf1)

Solutions

  1. Remove or defer any callback/listener that synchronously calls unlock() from within the unlock path — use a separate thread or queue.
  2. Wrap unlock() in your own guard so it is only called once per acquire.
  3. Audit AOP aspects and interceptors that wrap unlock() to ensure they do not re-enter.
  4. Ensure shutdown hooks do not call unlock() on a lock that is mid-unlock — track lifecycle state externally.

Example fix

// before
@Override
public void onLockReleased(String key) {
    lock.unlock(); // called from within unlock() → recursion detected
}

// after — defer the callback to avoid re-entrancy
@Override
public void onLockReleased(String key) {
    cleanupExecutor.submit(() -> {
        // handle release cleanup without re-entering unlock()
    });
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard against recursive unlock at the application level
private final AtomicBoolean unlockInProgress = new AtomicBoolean(false);

void safeUnlock(NacosLock lock) {
    if (!unlockInProgress.compareAndSet(false, true)) {
        return; // already unlocking — skip recursive call
    }
    try {
        lock.unlock();
    } finally {
        unlockInProgress.set(false);
    }
}

Type guard

// N/A — recursion is a control-flow issue, not a type-level concern.

Try / catch

try {
    lock.unlock();
} catch (IllegalMonitorStateException e) {
    if (e.getMessage().contains("Recursive unlock")) {
        logger.error("Recursive unlock detected for key={}", lock.getKey());
        // investigate the callback/listener that triggered re-entry
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: During unlock(), the gRPC unLockWithResult call or watchdog.unregister triggers a callback, listener, or shutdown hook that synchronously calls unlock() again on the same NacosLock instance from the same thread before the first unlock() has completed.

Common situations: A lock-state-change listener is registered that calls unlock() on notification; a shutdown hook fires during unlock() and attempts cleanup that includes unlock(); an AOP aspect or interceptor wraps unlock() and triggers a recursive call; a custom Lock wrapper delegates to unlock() and is itself called from within the unlock path.

Related errors


AI-assisted analysis of alibaba/nacos@9b989acdf1 (2026-08-14). Data as JSON: /api/errors/9b31ea0c9bc9e80c. Report an issue: GitHub.