锁粒度应尽量缩小,优先使用私有final对象或原子类替代粗粒度锁;读多写少场景用读写锁或volatile;避免锁竞争与jit优化冲突,需结合压测和jvm诊断工具调优。

锁粒度太大导致线程频繁阻塞
多数性能问题不是出在“没加锁”,而是“锁得太宽”。比如用 synchronized 修饰整个方法,或对一个大对象(如 HashMap 实例)长期持锁,会让本可并发执行的操作被迫串行。
实际中更合理的做法是缩小临界区:只对真正共享且需互斥访问的变量操作加锁,其余计算、IO、日志等移出同步块。例如更新计数器时,不要把数据库查询也包进 synchronized 块里。
- 避免用
this或类对象(MyClass.class)作为锁,除非明确需要全局互斥 - 优先使用私有 final 对象作锁:
private final Object lock = new Object(); - 考虑用
java.util.concurrent.atomic类(如AtomicInteger)替代简单计数场景,完全避开锁
读多写少场景下仍用悲观锁
当数据被读取远多于修改(如配置缓存、白名单),synchronized 或 ReentrantLock 会严重限制吞吐——每次读都要竞争锁,哪怕没人正在写。
这时应切换到读写分离模型:ReentrantReadWriteLock 允许多个读线程同时进入,仅写操作独占。但要注意它不保证重入顺序,且写锁降级需显式调用 writeLock().unlock() 后再拿读锁,不能自动转换。
立即学习“Java免费学习笔记(深入)”;
- 读锁可重入,但写锁不支持“升级”(即持有读锁时无法直接获取写锁,会死锁)
-
StampedLock提供乐观读(tryOptimisticRead),适合极短读操作且冲突极少的场景,但需手动校验戳记有效性 - 若读操作本身很轻(如只是返回一个
int字段),直接用volatile更高效,无需任何锁
锁竞争激烈时未启用自旋或队列优化
默认 ReentrantLock 是非公平的,线程唤醒后可能立即抢到锁,但也可能因上下文切换开销大而拖慢整体响应。高竞争下,短时间自旋等待比挂起线程更省资源。
JVM 的 -XX:PreBlockSpin(旧版)或现代 HotSpot 的自适应自旋逻辑会根据历史表现决定是否自旋,但前提是锁持有时间足够短。若临界区含 IO 或复杂计算,自旋反而浪费 CPU。
- 创建
ReentrantLock时传true启用公平模式(new ReentrantLock(true)),但会显著降低吞吐,仅用于防止线程饿死 - 用
LockSupport.parkNanos()手动控制等待精度,比Thread.sleep()更轻量,但需配合状态轮询 - 注意
ConcurrentHashMap在 JDK 8+ 已改用 CAS + synchronized 分段,不再依赖Segment,高并发下比手动分段锁更可靠
忽略锁与 GC、JIT 编译的交互影响
锁对象若频繁创建(如每次方法调用都 new 一个 Object 当锁),不仅增加 GC 压力,还可能导致 JIT 无法有效锁消除(lock elision)。JIT 只在逃逸分析确认锁对象不会被其他线程访问时,才可能去掉该锁。
同样,过度细粒度的锁(如每个元素一个锁)虽减少竞争,却可能让 JVM 难以内联或优化,反而降低性能。真实压测中,有时 4 段分段比 64 段更快。
- 避免在循环体内新建锁对象;锁对象应复用,且生命周期尽量长
- 用
jstat -compiler观察方法是否被 JIT 编译,未编译的同步代码不会触发锁优化 - 开启
-XX:+PrintCompilation和-XX:+UnlockDiagnosticVMOptions -XX:+PrintEscapeAnalysis可验证锁消除是否生效
锁优化不是堆参数或换工具就能解决的事——它高度依赖具体访问模式、数据规模和 JVM 运行时行为。最危险的误判,是把局部测试结果直接推广到生产流量分布完全不同的环境里。











