alibaba/nacos · error · IllegalMonitorStateException
Current thread does not hold the lock
Error message
Current thread does not hold the lock
What it means
Thrown by NacosLock.unlock() when the calling thread's ThreadLocal reentrant count is zero or less, meaning this thread never acquired the lock. NacosLock uses a per-thread reentrant counter (localReentrantCount ThreadLocal) so only the owning thread may release the lock; cross-thread handoff is intentionally unsupported. This mirrors java.util.concurrent.locks.ReentrantLock semantics where unlocking without prior locking is a programming error.
Source
Thrown at client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java:297
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;
}
} else {
localReentrantCount.set(0);
watchdog.unregister(key);
localReentrantCount.remove();
removed = true;
throw new IllegalMonitorStateException(
"Unlock rejected by server, key=" + key + ", msg="View on GitHub (pinned to 9b989acdf1)
Solutions
- Ensure the same thread that calls lock() also calls unlock(); wrap the critical section so acquisition and release are in the same stack frame.
- If using async/thread-pool handoff, do not use the JUC NacosLock for the release — use NacosLockService.remoteReleaseLock() which does not depend on the ThreadLocal count.
- Add a guard: only call unlock() inside a try-block whose try was preceded by a successful lock(); never call unlock() unconditionally in a finally without confirming the lock was acquired.
- If state may already be cleared (e.g. after an exception), check the lock ownership/heartbeat before calling unlock().
Example fix
// before
NacosLock lock = lockService.getReentrantLock(key);
try {
// work
} finally {
lock.unlock(); // may throw if lock() not called on this thread
}
// after
NacosLock lock = lockService.getReentrantLock(key);
boolean acquired = false;
try {
lock.lock();
acquired = true;
// work
} finally {
if (acquired) {
lock.unlock();
}
} Defensive patterns
Strategy: validation
Validate before calling
// NacosLock exposes no public isHeldByCurrentThread; track acquisition yourself
boolean acquired = false;
try {
nacosLock.lock();
acquired = true;
// critical section
} finally {
if (acquired) {
nacosLock.unlock(); // safe: same thread, count > 0
}
} Try / catch
try { nacosLock.unlock(); } catch (IllegalMonitorStateException e) { // thread did not hold the lock; log and continue, no cleanup needed } Prevention
- Always pair lock() and unlock() in the same try/finally on the same thread.
- For async/thread-pool release, use NacosLockService.remoteReleaseLock() instead of the JUC unlock().
- Never mix the JUC NacosLock API with raw LockGrpcClient.lock() on the same key.
When it happens
Trigger: Calling nacosLock.unlock() from a different thread than the one that called nacosLock.lock(); calling unlock() twice without a matching lock() on the same thread; using a NacosLock instance that was constructed but never locked; attempting to release a lock whose client-side state was already cleared by a prior failed/partial unlock.
Common situations: Async/callback architectures where acquisition and release happen on different threads (e.g. acquiring in a request thread, releasing in a completion-handler); thread-pool reuse where the ThreadLocal count from a previous task was already removed; mixing the JUC NacosLock API with the raw LockGrpcClient.lock() on the same key (warned against in the class Javadoc), which leaves the ThreadLocal count at zero.
Related errors
- Unlock rejected by server, key={}, msg={}
- Failed to unlock: {}
- Condition not supported in Nacos distributed lock
- NacosLockService has been shut down
- MCP server not found in registry: ${externalId}
AI-assisted analysis of alibaba/nacos@9b989acdf1 (2026-08-14).
Data as JSON: /api/errors/a917d3e53023bbbf.
Report an issue: GitHub.