高可用工业系统依赖对jvm内存行为和并发工具底层约束的克制设计,而非仅靠api调参;concurrenthashmap不适用于全局计数,应选longadder或分段key;trylock需配超时与fallback;g1的maxgcpausemillis是软目标;volatile不保证复合操作原子性。

直接说结论:靠背 API 和调参数堆不出高可用工业系统;真正稳的系统,是靠对 JVM 内存行为、java.util.concurrent 工具类的底层约束条件有明确判断后,做克制设计换来的。
为什么 ConcurrentHashMap 不能当全局计数器用
很多人拿 ConcurrentHashMap 存统计指标(比如请求总数、失败次数),结果压测时发现数值不准或增长缓慢。这不是 bug,是误用了它的设计前提:
- 它保证的是「单个 key 的 put/get 操作线程安全」,不是「多个 key 的复合操作原子性」
-
map.get("a") + map.get("b")这种读-读组合,中间可能被其他线程修改,结果不可信 - 高并发写同一 key(如
map.compute("count", (k, v) -> v == null ? 1 : v + 1))在 JDK8+ 虽用 CAS,但冲突重试成本会上升,不如LongAdder - 如果真要全局计数,优先选
LongAdder;要带维度聚合,用ConcurrentHashMap+ 分段 key(如"host:192.168.1.10:qps"),再由外部合并
ReentrantLock 的 tryLock 不是“加锁失败就跳过”的万能解
tryLock() 常被用来避免阻塞,但工业级系统里滥用会导致状态不一致:
- 它只保证「当前时刻无锁可得」,不代表业务逻辑可以安全跳过——比如库存扣减,跳过可能等于漏扣
- 没配超时的
tryLock()在锁竞争激烈时几乎总返回 false,等于把压力转给上游重试逻辑 - 真正该用
tryLock(long, TimeUnit),且必须配套 fallback:例如降级为数据库行锁、或走异步补偿队列 - 注意公平锁模式(
new ReentrantLock(true))会显著降低吞吐,仅在必须保序场景才启用
G1 GC 的 -XX:MaxGCPauseMillis 是软目标,不是 SLA 承诺
很多团队把 -XX:MaxGCPauseMillis=200 当作延迟保障,结果线上偶发 800ms STW。这是因为:
- 该参数只是 G1 的启发式目标,实际暂停时间受堆大小、对象存活率、混合回收比例共同影响
- 当老年代碎片化严重或跨代引用过多时,G1 会放弃目标,强制进入 Full GC(尤其 JDK8u222 之前版本)
- 必须配合
-Xlog:gc*=debug观察mixed gc是否频繁、evacuation failure是否出现 - 超过 8GB 堆且延迟敏感,直接切
-XX:+UseZGC;ZGC 的停顿基本与堆大小无关,但需确认 OS 支持大页(/proc/sys/vm/max_map_count)
volatile 不能替代 synchronized 的唯一原因:它不保证原子性
常见错误是认为 “加了 volatile 就线程安全”,比如:
private volatile int counter = 0;
public void increment() {
counter++; // 非原子:读-改-写三步,volatile 只保最后写可见
}
这会导致计数丢失。正确做法取决于场景:
- 纯计数 → 用
AtomicInteger或LongAdder - 需要复合逻辑(如“先检查再更新”)→ 必须用
synchronized或ReentrantLock - 仅作状态标记(如
isRunning)→volatile足够,且比锁轻量 - 注意:
volatile对long和double的 64 位写是原子的(JVM 规范保证),但不意味复合操作安全
最常被忽略的一点:所有这些机制的边界,都依赖于你是否清楚「当前代码运行在哪一层内存模型上」——是 CPU 缓存行?JVM 工作内存?还是 GC 后的堆布局?不查 jstat、不看 GC 日志、不跑 JMH 基准测试,就调 ConcurrentHashMap 的 concurrencyLevel 或改 ThreadPoolExecutor 的队列类型,本质上是在猜。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











