jstack -l 可一眼定位java线程死锁,关键看“found 1 deadlock.”或多个blocked线程等待同一锁地址。

Java线程死锁怎么一眼定位?
用 jstack 是最直接的办法。当程序卡住、响应变慢,先别急着翻代码,立刻执行:jstack -l <pid></pid>。注意加 -l 参数——它能显示显式锁(ReentrantLock)的持有和等待关系,而默认输出只展示 synchronized 块。
关键看输出里有没有 “Found 1 deadlock.” 段落;如果没有,就扫一遍所有线程状态,重点找多个线程都处于 BLOCKED 状态,且都在等同一个 java.lang.Thread.State: BLOCKED (on object monitor) 地址的情况。
- 常见陷阱:JVM 进程可能被误杀或重启,
jstack必须在问题复现时立刻抓取,延迟几秒就可能错过现场 - 如果应用跑在容器里,得进容器执行,或者用
docker exec -it <container> jstack -l 1</container>(假设 Java 进程 PID 是 1) -
jstack输出中锁对象地址如0x0000000712345678,要和线程堆栈里的waiting to lock对应上才构成证据链
为什么 ConcurrentHashMap 还会出并发 bug?
很多人以为用了 ConcurrentHashMap 就万事大吉,结果发现 get 到 null、计数不准、甚至 NullPointerException。根本原因在于:它只保证单个操作(put、get、remove)线程安全,不保证复合操作原子性。
比如 if (!map.containsKey(key)) map.put(key, value); 这种“检查-执行”逻辑,中间完全可能被其他线程插入,导致重复写入或覆盖。
立即学习“Java免费学习笔记(深入)”;
- 正确做法是用
map.putIfAbsent(key, value)替代 if-put 模式 - 需要条件更新时,优先考虑
computeIfAbsent或merge,它们内部已做 CAS 或锁分段保障 - 如果必须手动同步,别锁
map实例本身(易引发死锁),改用私有锁对象或synchronized(map.getClass())(仅限极简单场景)
ThreadLocal 内存泄漏的真实诱因是什么?
不是因为没调 remove() 就一定会泄漏,而是当 ThreadLocal 变量被设为 null 后,其对应的 Entry 在线程的 ThreadLocalMap 中仍存在,且 key 是弱引用、value 是强引用——只要线程长期存活(比如线程池里的线程),value 就一直被 hold 住,无法 GC。
典型场景:Web 应用中,在 Filter 或 Interceptor 里 set 了一个大对象到 ThreadLocal,但忘记 remove,请求结束后该线程归还线程池,下一次复用时旧 value 仍在。
- 必须在 finally 块里调
threadLocal.remove(),不能只靠 try-with-resources(ThreadLocal不实现AutoCloseable) - Tomcat 等容器会在请求结束时自动清理部分
ThreadLocal,但只针对它自己注册的,你自定义的仍需手动管理 - 排查时可 dump heap 后用 MAT 搜索
java.lang.ThreadLocal$ThreadLocalMap$Entry,看 value 是否持有不该存在的大对象
用 CompletableFuture 时异步链突然中断却不报错?
这是最隐蔽的排错点:supplyAsync 或 thenApply 抛异常后,若没显式处理,异常会被吞掉,后续 thenAccept 不执行,也没有日志,整个链静默失败。
根本原因是 CompletableFuture 的异常默认只保存在内部 state 中,不会自动传播或打印;只有调用 join()、get() 或注册 exceptionally/handle 才会暴露。
- 所有异步链末端务必加
whenComplete((r, e) -> { if (e != null) log.error("async failed", e); }) - 避免在
thenRun或thenAccept里写可能抛异常的逻辑,真要写,包一层 try-catch 并记录 - 测试时别只看最终结果是否正确,要用
CountDownLatch等方式强制等待 completion,再断言异常是否被正确捕获
并发问题往往不在代码写错,而在对工具类行为边界的误解。比如以为 CopyOnWriteArrayList 适合高频写,其实它每次写都复制数组,写多读少反而拖垮性能;又比如在 synchronized 方法里调远程服务,把锁范围无意扩大成服务响应时间。这些细节不通过真实压测和线程 dump 很难发现。











