{"record":{"id":"a917d3e53023bbbf","repo":"alibaba/nacos","slug":"current-thread-does-not-hold-the-lock","errorCode":null,"errorMessage":"Current thread does not hold the lock","messagePattern":"Current thread does not hold the lock","errorType":"exception","errorClass":"IllegalMonitorStateException","httpStatus":null,"severity":"error","filePath":"client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java","lineNumber":297,"sourceCode":"                grpcClient.cancelWait(key, lockType, currentOwner());\n                localReentrantCount.remove();\n                LOGGER.error(\"Failed to try lock with timeout, key={}\", key, e);\n                return false;\n            }\n        }\n    }\n    \n    @Override\n    public void unlock() {\n        if (inUnlock.get()) {\n            throw new IllegalMonitorStateException(\"Recursive unlock() detected for key=\" + key);\n        }\n        inUnlock.set(Boolean.TRUE);\n        boolean removed = false;\n        try {\n            int count = localReentrantCount.get();\n            if (count <= 0) {\n                throw new IllegalMonitorStateException(\"Current thread does not hold the lock\");\n            }\n            try {\n                LockInstance instance = buildInstance(0);\n                LockResult result = grpcClient.unLockWithResult(instance);\n                if (result.isSuccess()) {\n                    localReentrantCount.set(count - 1);\n                    if (result.getReentrantCount() == 0) {\n                        watchdog.unregister(key);\n                        localReentrantCount.remove();\n                        removed = true;\n                    }\n                } else {\n                    localReentrantCount.set(0);\n                    watchdog.unregister(key);\n                    localReentrantCount.remove();\n                    removed = true;\n                    throw new IllegalMonitorStateException(\n                        \"Unlock rejected by server, key=\" + key + \", msg=\"","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/alibaba/nacos/blob/9b989acdf181d00898f2e8839257bb2b2a3cefe3/client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java#L279-L315","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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()."],"exampleFix":"// before\nNacosLock lock = lockService.getReentrantLock(key);\ntry {\n    // work\n} finally {\n    lock.unlock(); // may throw if lock() not called on this thread\n}\n\n// after\nNacosLock lock = lockService.getReentrantLock(key);\nboolean acquired = false;\ntry {\n    lock.lock();\n    acquired = true;\n    // work\n} finally {\n    if (acquired) {\n        lock.unlock();\n    }\n}","handlingStrategy":"validation","validationCode":"// NacosLock exposes no public isHeldByCurrentThread; track acquisition yourself\nboolean acquired = false;\ntry {\n    nacosLock.lock();\n    acquired = true;\n    // critical section\n} finally {\n    if (acquired) {\n        nacosLock.unlock(); // safe: same thread, count > 0\n    }\n}","typeGuard":null,"tryCatchPattern":"try { nacosLock.unlock(); } catch (IllegalMonitorStateException e) { // thread did not hold the lock; log and continue, no cleanup needed }","preventionTips":["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."],"tags":["lock","thread-local","reentrant","illegal-state","distributed-lock"],"backgroundTag":null,"analyzedSha":"9b989acdf181d00898f2e8839257bb2b2a3cefe3","analyzedAt":"2026-08-14T07:17:31.569Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}