不能直接用 synchronized 方法锁整个函数,因为会将无关的IO、计算等逻辑也串行化,降低吞吐量;应仅对真正操作共享变量的代码段加同步块,并选用私有final对象作锁,避免死锁与性能陷阱。

为什么不能直接用 synchronized 方法锁整个函数
因为 synchronized 方法会把整个方法体当成临界区,哪怕只有几行真正操作共享变量,其余逻辑(比如 IO、计算、日志)也被强行串行化,严重拖慢吞吐。尤其在高并发场景下,锁粒度越粗,线程等待越久,CPU 利用率越低。
典型反例:public synchronized void processOrder(Order order) —— 如果里面包含调用外部 HTTP 接口或解析 JSON,这些完全不需要同步,却成了锁瓶颈。
实操建议:
- 只对真正读写共享状态的代码段加
synchronized块,比如更新计数器、修改HashMap、变更对象字段 - 把耗时操作(网络、磁盘、复杂计算)移出同步块,哪怕要拆成“读-处理-写”三步
- 避免在同步块内调用外部可阻塞或不可控的方法(如
service.doSomething()),以防锁被意外长时间持有
怎么选对锁对象:this、Class 还是私有 final 对象
锁对象决定了同步的作用域和竞争范围。用错会导致该串行的没串行,不该串行的被强绑在一起。
常见错误现象:synchronized(this) 在非私有类中暴露锁对象,外部代码可能恶意加锁导致死锁;synchronized(MyClass.class) 让所有实例共用一个类锁,丧失实例级并发能力。
实操建议:
- 优先用私有
final Object lock = new Object()作为锁,作用域清晰、不可被外部访问 - 需要保护静态资源时才用
MyClass.class,且明确注释“此为类级别锁” - 仅当同步逻辑天然属于当前实例行为(如单例内部状态管理)且确定无外部干扰时,才考虑
this - 切勿用可变对象(如
String、集合、数组)作锁,它们的equals/hashCode或内容变化可能导致锁失效或不一致
嵌套同步块和锁顺序:为什么看似安全的代码还是会死锁
两个线程分别按不同顺序获取多个锁,就可能形成环路等待。比如线程 A 先锁 lockA 再尝试 lockB,线程 B 反过来先锁 lockB 再等 lockA——此时 JVM 无法自动解耦,只能卡死。
实操建议:
- 所有需要多锁的场景,强制约定全局锁顺序,例如“总是先获取
accountLock,再获取logLock” - 用
tryLock()(配合java.util.concurrent.locks.ReentrantLock)替代嵌套synchronized,能设置超时并主动退出,避免无限等待 - 同步块尽量扁平,避免在
synchronized块里再调用另一个含同步逻辑的方法,除非你能 100% 确认其锁对象和顺序 - 用
jstack抓线程快照时重点看waiting to lock和locked ownable synchronizers段,快速定位死锁源头
性能陷阱:synchronized 块里做太多事,比你想象中更伤
JVM 虽然对 synchronized 做了锁消除、偏向锁、轻量级锁等优化,但一旦发生真实竞争(多个线程反复抢同一把锁),就会膨胀为重量级锁,触发 OS 级线程挂起/唤醒,开销从纳秒级跳到微秒甚至毫秒级。
实操建议:
- 用
System.nanoTime()或 JMH 测量同步块实际执行时间,如果平均 > 10μs,就要怀疑是否混入了不该有的逻辑 - 高频小对象创建(如
new StringBuilder())、字符串拼接、装箱拆箱等看似轻量的操作,在同步块里累积起来也会显著拉长持锁时间 - 考虑用无锁方案替代:如计数用
AtomicInteger、集合用ConcurrentHashMap、队列用LinkedBlockingQueue,它们内部已做精细化同步或 CAS 优化 - 如果同步逻辑不可避免且复杂,宁可拆成多个细粒度锁 + 分段控制,也不要图省事包一大块
真正难的不是写对 synchronized 块,而是判断哪一行代码真的需要它——多数时候,问题不在锁太细,而在我们没想清楚哪些状态必须原子性,哪些其实可以异步、缓存或最终一致。

















