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

  1. Ensure the same thread that calls lock() also calls unlock(); wrap the critical section so acquisition and release are in the same stack frame.
  2. 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.
  3. 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.
  4. 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

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


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