定位本地锁泄漏的核心是确认“本地锁对象”是否被长期持有且未清理,同时被全局监控Map强引用;需检查Map大小是否持续增长、ThreadLocal是否未正确remove、释放逻辑是否被异常绕过。
定位这类泄漏,核心是确认“本地锁对象”是否被长期持有、未清理,且该对象仍被全局监控 map 强引用。这里的“本地锁对象”通常指业务中自定义的锁封装(如 lockholder、lease 或基于 threadlocal + 全局注册表的上下文对象),而非 redis 键本身。
检查全局监控 Map 的存活对象数量是否持续增长
这是最直接的信号。如果该 Map 是静态的或单例管理的(如 ConcurrentHashMap<string lockref></string>),需重点关注其 size 是否随时间线性上升,尤其在高并发加锁/释放周期后未回落。
- 通过 JVM 监控工具(如 JConsole、VisualVM)连接运行中的服务,查看该 Map 实例的 entrySet 大小变化趋势
- 用
jmap -histo <pid>统计对象实例数,筛选出疑似锁包装类(如com.xxx.LockContext)是否异常增多 - 添加日志埋点:在 Map 的
put和remove调用处记录 key、堆栈和时间戳,观察是否有put频繁但remove缺失的情况
确认锁对象与 ThreadLocal 的耦合关系是否断裂
很多实现会将锁对象存入 ThreadLocal,并在业务结束时通过 remove() 触发全局 Map 清理。一旦 ThreadLocal 没有正确清理,就可能造成“锁对象还在 Map 里,但已无人认领”。
- 检查相关
ThreadLocal<?>变量是否声明为static,且其set()后未配对调用remove() - 特别注意异步场景(如
CompletableFuture、线程池提交任务):主线程的ThreadLocal不会自动传递到子线程,若子线程内注册了锁但未显式清理,Map 就会残留 - 用 MAT(Memory Analyzer Tool)分析 heap dump,按
ThreadLocalMap$Entry过滤,查看 value 是否为锁对象,key 是否为已失效的ThreadLocal实例(弱引用已回收,但 value 仍强引用着锁)
验证锁释放流程是否真正执行到 Map 移除逻辑
分布式锁释放通常分两步:Redis 层删 key + 本地上下文清理。后者容易被忽略或被异常绕过。
- 检查释放方法(如
unlock())是否包裹在try-finally中,确保即使业务抛异常,清理逻辑也能执行 - 排查是否有“提前 return”或“吞异常”代码块跳过了
globalLockMap.remove(key) - 若使用 AOP 统一拦截加锁/解锁,确认切点表达式是否覆盖所有分支(如不同 Controller 方法、定时任务、消息监听器)
模拟压测并对比 GC 前后 Map 状态
在测试环境复现问题,有助于快速验证假设。
- 启动压测(如 1000 QPS 持续加锁/释放 5 分钟),期间多次执行
jstat -gc <pid>,观察老年代使用率是否缓慢爬升 - 压测结束后触发一次
System.gc()(仅用于诊断),再查 Map size —— 若 size 不变,说明对象被强引用持有,未被 GC;若 size 下降,则可能是短生命周期对象堆积,需优化复用 - 结合
jstack <pid>查看是否有线程长时间阻塞在锁等待或释放逻辑中,导致清理延迟

















