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
- Remove or defer any callback/listener that synchronously calls unlock() from within the unlock path — use a separate thread or queue.
- Wrap unlock() in your own guard so it is only called once per acquire.
- Audit AOP aspects and interceptors that wrap unlock() to ensure they do not re-enter.
- 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
- Never call unlock() from within a callback or listener triggered by the unlock path.
- Defer cleanup work to a separate executor rather than re-entering unlock() synchronously.
- Wrap unlock() in an application-level guard flag to prevent double-calls.
- Audit AOP aspects and shutdown hooks for re-entrant unlock() calls.
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
- Non-reentrant lock does not allow reentry on the same thread
- Lock interrupted
- Failed to acquire lock: {}
- 30000
- 20005
AI-assisted analysis of alibaba/nacos@9b989acdf1 (2026-08-14).
Data as JSON: /api/errors/9b31ea0c9bc9e80c.
Report an issue: GitHub.