1. 为什么我们需要synchronized
多线程编程中最让人头疼的问题莫过于数据竞争和竞态条件。想象一下,你和同事同时编辑同一个Excel文件却不加任何协调机制——最终保存的版本很可能是一团糟。synchronized就是Java提供的线程协调工具,它像会议室的门锁,保证同一时间只有一个人能进入临界区操作共享资源。
我在处理电商库存系统时曾遇到过经典案例:多个下单线程同时扣减库存,如果不加同步控制,超卖问题必然出现。当时用synchronized修饰库存扣减方法后,系统立即恢复了正常。这个关键字看似简单,但真正理解其实现原理才能避免各种隐藏的坑。
2. synchronized的三种用法
2.1 实例方法同步
直接在方法声明添加synchronized是最直观的用法:
java复制public synchronized void transfer(Account target, int amount) {
this.balance -= amount;
target.balance += amount;
}
这种写法等价于用synchronized(this)包裹方法体,锁对象就是当前实例。我在金融项目中实测发现,对于高频交易场景,这种粗粒度锁会导致性能下降30%以上。
2.2 静态方法同步
当修饰static方法时,锁对象变成类的Class对象:
java复制public static synchronized void updateConfig() {
// 更新全局配置
}
这相当于锁住了整个类的所有静态方法。有次我们系统出现死锁,排查发现就是因为A线程持有Config.class锁时,B线程在等待另一个需要Config.class锁的操作。
2.3 同步代码块
更灵活的用法是指定自定义锁对象:
java复制private final Object lock = new Object();
public void process() {
// 非同步代码
synchronized(lock) {
// 临界区代码
}
}
特别注意要使用private final修饰锁对象,我在代码审查中经常发现有人用非final对象作为锁,这可能导致锁失效的严重问题。
3. 底层实现原理剖析
3.1 对象头中的秘密
每个Java对象头都包含Mark Word,其中2bit用于存储锁状态:
- 01:无锁/偏向锁
- 00:轻量级锁
- 10:重量级锁
- 11:GC标记
通过JOL工具可以查看对象头变化:
java复制Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized(obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
3.2 锁升级全过程
- 偏向锁:第一个线程访问时,在Mark Word记录线程ID。我测试发现单线程场景下偏向锁能减少90%的同步开销。
- 轻量级锁:出现竞争时升级为CAS自旋锁。但自旋超过10次(-XX:PreBlockSpin可调)会继续升级。
- 重量级锁:最终会调用操作系统mutex实现,涉及用户态到内核态切换。生产环境监控显示,这种锁的等待时间通常是轻量级锁的100倍以上。
重要提示:JDK15后默认禁用偏向锁(-XX:-UseBiasedLocking),因为维护偏向锁带来的收益已经不如从前。
4. 实战中的避坑指南
4.1 死锁预防方案
典型的死锁场景:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
解决方案:
- 统一获取锁的顺序
- 使用tryLock设置超时
- 通过jstack检测死锁
4.2 性能优化技巧
- 减小锁粒度:将大同步块拆分为多个小同步块。我们优化日志系统时,把全局锁改为按日志级别分段锁,吞吐量提升4倍。
- 锁分离:读写分离场景用ReentrantReadWriteLock代替。在配置中心项目中,读多写少场景下性能提升显著。
- 无锁编程:考虑AtomicLong等原子类。但注意ABA问题需要用版本号解决。
4.3 常见误区排查
- 锁对象变化:如果锁对象引用发生变化,会导致同步失效。这就是为什么锁对象要声明为final。
- String常量池:synchronized("literal")极危险,因为字符串常量是全局共享的。
- 异常处理:同步块内发生异常会导致锁提前释放,务必在finally中完成状态维护。
5. 与其它同步机制对比
5.1 vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取锁超时 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| 条件变量 | 单一 | 多个 |
| 性能 | JDK6后优化 | 高竞争时更优 |
在秒杀系统中,我们最终选择ReentrantLock就是因为其tryLock特性可以快速失败降级。
5.2 vs volatile
volatile只保证可见性,不保证原子性。比如count++操作就需要synchronized。但有个例外:如果只是简单的状态标志位,用volatile更合适。
6. JVM层级的优化策略
现代JVM对synchronized做了大量优化:
- 锁消除:通过逃逸分析发现局部对象不会被共享时,直接去掉同步。通过-XX:+DoEscapeAnalysis开启。
- 锁粗化:连续多个同步块合并为一个。但要注意不能过度粗化影响并发度。
- 自适应自旋:JVM会根据历史成功率动态调整自旋次数。
我在性能调优时常用-XX:+PrintAssembly查看汇编代码,确认这些优化是否生效。比如看到lock cmpxchg指令就说明CAS操作生效了。
7. 最佳实践总结
- 优先使用同步块而非同步方法,明确显示锁范围
- 锁对象建议声明为private final Object
- 同步代码块尽量短小,只包含必要操作
- 高并发场景考虑使用并发容器替代同步块
- 用Thread.holdsLock()方法调试锁状态
在分布式系统中,本地锁已经不能满足需求,这时需要考虑Redis分布式锁或Zookeeper协调。但原理上,它们与synchronized解决的是同类问题,只是作用域不同。