CountDownLatch适用于一次性等待(如服务初始化),CyclicBarrier支持重复使用(如多轮分片计算);computeIfAbsent不可用于高并发IO操作;thenCompose用于扁平化CompletableFuture嵌套;ThreadLocal在线程池中必须手动remove以防内存泄漏。

用 CountDownLatch 还是 CyclicBarrier?看是否需要复用
两者都用于线程等待,但语义和生命周期完全不同。CountDownLatch 是一次性门闩:计数归零后所有等待线程被释放,之后再调用 await() 会立即返回;CyclicBarrier 支持重复使用,每次拦截满指定数量线程后自动重置。
常见错误是把 CountDownLatch 当作循环同步点——比如在每轮批处理中反复 await(),结果第二轮直接跳过,逻辑错乱。
- 选
CountDownLatch:启动阶段等 N 个服务初始化完成(只等一次) - 选
CyclicBarrier:多线程分片计算,每轮需全部线程到达后再统一汇总(如迭代算法、模拟步进) -
CyclicBarrier可选传入Runnable,在每次屏障触发时执行一次全局动作(如日志、状态检查)
ConcurrentHashMap 的 computeIfAbsent 别在高并发下当“懒加载锁”用
这个方法看似能原子地判断 + 插入,但它的实现不是无锁的:内部会锁住对应桶(bin),若计算逻辑耗时(比如远程调用、IO),会导致该哈希桶长期被阻塞,拖慢其他 key 的读写。
典型误用:
cache.computeIfAbsent(key, k -> fetchFromDatabase(k));在高并发查缓存场景下,可能引发大量线程在同一个桶上排队。
立即学习“Java免费学习笔记(深入)”;
- 真正适合的场景:计算轻量、无副作用(如构建一个简单对象、解析固定字符串)
- 含 IO 或不确定耗时的操作,应先用
get()判断,再用显式锁(如ReentrantLock)或putIfAbsent()+ 双重检查 - JDK 9+ 的
computeIfAbsent对递归调用做了防护,但不解决性能阻塞问题
CompletableFuture 链式调用里,thenApply 和 thenCompose 混用导致嵌套 CompletableFuture
这是最常踩的类型陷阱。如果异步操作本身返回 CompletableFuture,却用了 thenApply,结果会是 CompletableFuture,后续 join() 会抛 ClassCastException 或死锁。
正确做法是:返回 CompletableFuture 就必须用 thenCompose(它会“压平”一层);返回普通值才用 thenApply。
-
thenApply(x -> service.doSync(x))→ 返回T,用thenApply -
thenApply(x -> service.doAsync(x))→ 返回CompletableFuture,必须改thenCompose - 漏掉这层判断,调试时看到
java.util.concurrent.CompletableFuture@12345678打印出来,基本就是嵌套了
ThreadLocal 在线程池里不清理,内存泄漏比想象中来得快
很多人以为只在 ThreadLocal.set() 后忘记 remove() 才泄漏,其实更隐蔽:只要 ThreadLocal 实例本身是静态的(比如单例工具类里的 private static final ThreadLocal),而线程来自线程池(如 Executors.newFixedThreadPool),那么每个工作线程的 ThreadLocalMap 就会长期持有该变量的强引用,且 key 是弱引用 —— key 被回收后,value 却滞留,形成“幽灵引用”,GC 清不掉。
- 所有在线程池中使用的
ThreadLocal,必须在任务结束前调用remove()(推荐放在try-finally块里) - 不要依赖
initialValue()自动创建,它只在第一次get()时触发,无法保证后续复用时状态干净 - Spring 的
RequestContextHolder等封装已内置清理,但自定义ThreadLocal必须手动管
CyclicBarrier 的重入代价、computeIfAbsent 的桶级锁粒度、CompletableFuture 的泛型嵌套规则、ThreadLocal 在线程复用场景下的引用链残留——这些不在文档首页,但在压测或上线后突然冒头。










