ReentrantLock未释放不会导致内存泄漏,而是引发线程阻塞与锁泄漏,表现为线程池耗尽、请求堆积;排查需用jstack查WAITING/BLOCKED线程,确认锁持有状态,确保unlock在finally中并校验isHeldByCurrentThread()。

ReentrantLock 未释放不会直接导致内存泄漏,但会引发线程阻塞、锁泄漏(lock leak),进而表现为线程池耗尽、请求堆积、CPU低而业务停滞——这种“伪内存泄漏”常被误判为内存问题。排查核心是区分:锁没释放 → 线程卡在 lock() → 线程数持续增长 → 线程泄漏;而真正内存泄漏需另查堆对象引用链。
看线程栈:用 jstack 快速识别锁持有者
这是最直接有效的第一步:
- 执行
jps -l找到目标 Java 进程 PID - 执行
jstack <pid> > thread_dump.txt</pid>生成线程快照 - 搜索关键词:
java.util.concurrent.locks.ReentrantLock$NonfairSync或parking to wait for,重点关注状态为WAITING或BLOCKED且堆栈停留在lock.lock()的线程 - 若发现大量线程卡在同一个
ReentrantLock实例上(例如 nid=0x... 指向同一锁对象哈希值),说明该锁长期被某线程持有未释放
查锁持有状态:确认是否真没释放
ReentrantLock 不像 synchronized 有 JVM 自动兜底,它完全依赖程序员手动释放。常见未释放场景包括:
-
没写 finally:把
unlock()放在 try 块末尾或 catch 里,一旦业务逻辑抛异常或提前 return,就跳过释放 -
tryLock() 后忘记释放:即使
tryLock()返回 true,也必须配对unlock(),且仍需放在 finally 中 -
重入次数不匹配:同一线程调用
lock()3 次,却只unlock()2 次,锁实际未完全释放 - 锁对象为空或复用错误:比如在 if 分支中初始化 lock,else 分支未初始化却调用 unlock,触发 NPE 或逻辑跳过
加防御性检查:运行时暴露未持锁调用
在 finally 中加 isHeldByCurrentThread() 判断,既能防止非法 unlock 报错,也能辅助定位问题路径:
立即学习“Java免费学习笔记(深入)”;
lock.lock();
try {
// 业务逻辑(可能 throw / return / continue)
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这个检查开销极小,但能避免因中断导致 lockInterruptibly() 失败后误 unlock;更重要的是,如果某处日志显示 “进入 finally 但 isHeldByCurrentThread() 为 false”,说明那条执行路径根本没拿到锁——可反推加锁逻辑有分支遗漏。
结合监控与日志:建立长期可观测性
单次 dump 只能抓瞬态,线上需主动埋点:
- 对关键 ReentrantLock 实例封装一层代理,记录每次
lock()和unlock()的线程 ID、时间戳、堆栈(采样) - 暴露 JMX 属性,如
getHoldCount()、isLocked()、getQueueLength(),用 Prometheus 定期采集 - 当
getQueueLength()持续 > 5 或isLocked()为 true 超过 30 秒,触发告警并自动 dump 线程 - 在业务入口/出口打日志,标记“lock acquired by [thread]”和“lock released”,缺失 release 日志即为线索
本质上,ReentrantLock 泄漏不是内存问题,而是控制流缺陷。它不涨堆内存,但会让线程永远挂起,最终拖垮整个服务。修复的关键不在 GC 工具,而在代码结构——lock 必须在 try 外,unlock 必须在 finally 内,且加持有校验。不复杂但容易忽略。


















