同步块应仅包含变更共享状态的最小原子操作,锁对象须私有、唯一、不可变,推荐private final Object lock = new Object();临界区外移日志、网络调用等非关键语句,避免嵌套锁与隐式竞争。

直接用 synchronized 普通代码块控制局部锁变量,核心不是“怎么写语法”,而是“在哪加、锁什么、放哪些语句”。重点在于把竞争压缩到真正共享状态变更的最小原子操作上,同时让锁对象本身不暴露、不复用、不误判。
锁对象必须私有、唯一、不可变
不能用 this、不能用局部变量、不能用可变对象(如 Integer、String、集合引用)作锁。它们要么导致意外竞争,要么根本不起作用。
- ✅ 推荐写法:private final Object lock = new Object(); —— 生命周期与实例绑定,外部不可见,语义清晰
- ✅ 特定场景可用业务对象作锁(如
account),但前提是该对象在多线程间稳定存在且不会被替换 - ❌ 避免
synchronized(new Object())或synchronized("key")—— 每次新建或字符串常量池复用,都会让锁失效或扩大竞争范围
临界区只留真正读写共享状态的语句
同步块里只能有“不执行完就破坏数据一致性”的那几行。其余一切——日志、计算、网络调用、对象构造、非共享字段赋值——全得挪出去。
- 错误示例:
synchronized(lock) {
log.info("update start");
balance += amount;
notifyAll();
sendNotification(); // 外部服务调用}
→ 日志和通知完全不需要锁保护,却拖长了持锁时间 - 正确拆分:
log.info("update start");synchronized(lock) {
balance += amount;
notifyAll();}sendNotification();
避免嵌套锁与隐式竞争
一个同步块里不要调用另一个可能加锁的方法,尤其不能调用你无法掌控实现逻辑的外部服务。
- 如果被调方法内部也用了
synchronized,且锁对象不同,极易形成死锁(A 锁 X 再等 Y,B 锁 Y 再等 X) - 即使锁对象相同,也会延长整体持锁时间,放大串行瓶颈
- 更稳妥的做法:先在同步块内完成共享状态变更,再出块后调用外部逻辑
验证是否真的缩到了最小
不能靠肉眼判断“看起来不多”,要结合运行时表现和压测数据:
- 用
Arthas thread -b或 JFR 查看线程是否在synchronized块内长时间处于BLOCKED或WAITING - 临时移出疑似冗余语句,对比 QPS 和平均响应时间变化;若性能无提升,说明它本就不该在锁里
- 检查是否把只读操作也锁了——多数纯读场景无需同步,除非涉及多个变量的复合一致性(这时考虑
volatile或StampedLock)










