1. Java锁机制概述
在Java并发编程的世界里,synchronized关键字就像一位经验丰富的交通警察,指挥着多线程环境下的数据访问秩序。自JDK 1.0时代起,它就作为内置的同步机制守护着线程安全,历经二十多年的演进,从最初的"重量级锁"逐步优化为如今智能高效的锁升级机制。
记得我刚接触多线程编程时,曾遇到一个令人头疼的bug:多个线程同时修改账户余额导致数据不一致。正是synchronized帮我解决了这个问题,让我深刻体会到理解锁机制的重要性。与后来出现的ReentrantLock等工具相比,synchronized的优势在于它的简单直接——不需要手动加锁解锁,编译器会自动插入异常处理,确保锁一定会被释放。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized的三种用法
2.1 实例方法同步
当synchronized修饰实例方法时,锁对象就是当前实例(this)。这种用法最适合保护对象内部状态的一致性。比如在电商系统中,商品库存的扣减就必须保证原子性:
java复制public class Product {
private int stock;
public synchronized void reduceStock(int quantity) {
if (stock >= quantity) {
stock -= quantity;
} else {
throw new IllegalStateException("库存不足");
}
}
}
这里有个实际开发中的经验:同步方法不宜过长,否则会成为性能瓶颈。我曾经优化过一个系统,将长达200行的同步方法拆分成几个小的同步块,吞吐量提升了3倍。
2.2 静态方法同步
静态同步方法锁住的是类的Class对象,适用于保护静态变量的线程安全。比如实现线程安全的单例模式:
java复制public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
需要注意的是,静态同步方法和实例同步方法使用的是不同的锁,它们之间不会相互阻塞。这个特性可以用来实现细粒度的锁控制。
2.3 同步代码块
同步代码块提供了最灵活的锁控制方式,可以指定任意对象作为锁。我常用这种方式来减小锁粒度:
java复制public class OrderService {
private final Map<Long, Order> orderMap = new HashMap<>();
private final Object lock = new Object();
public void processOrder(long orderId) {
// 非同步操作
Order order = getOrder(orderId);
synchronized(lock) {
// 同步操作
updateOrderStatus(order);
}
// 后续非同步操作
sendNotification(order);
}
}
特别提醒:千万不要用String或基本类型包装类作为锁对象,因为它们可能被JVM缓存重用,导致意外的锁竞争。曾经有同事用Integer.valueOf(1)作为锁,结果不同地方的代码莫名其妙地互相阻塞。
3. 对象头与Monitor机制
3.1 Java对象内存布局
每个Java对象在内存中都由三部分组成:对象头、实例数据和对齐填充。通过JOL工具可以直观查看:
java复制// 添加依赖:org.openjdk.jol:jol-core
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
在64位JVM(开启压缩指针)下,普通对象的内存布局如下:
- 对象头(12字节):Mark Word(8字节) + 类指针(4字节)
- 实例数据:根据字段类型计算
- 对齐填充:使总大小为8的倍数
3.2 Mark Word详解
Mark Word就像对象的"身份证",存储着关键的运行时信息。它的结构会随锁状态变化:
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | hashCode+分代年龄 | 01 |
| 偏向锁 | 线程ID+时间戳+分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向Monitor对象的指针 | 10 |
| GC标记 | 空(用于垃圾回收) | 11 |
通过代码可以观察Mark Word的变化:
java复制Object lock = new Object();
System.out.println("初始状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized(lock) {
System.out.println("加锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
3.3 Monitor工作原理
Monitor是synchronized的核心实现机制,每个Java对象都关联一个Monitor。它的关键组件包括:
- _owner:当前持有锁的线程
- _count:重入次数计数器
- _EntryList:等待获取锁的线程队列
- _WaitSet:调用了wait()的线程集合
当线程尝试获取锁时:
- 如果_owner为null,CAS设置_owner为当前线程
- 如果_owner已是当前线程,_count加1(锁重入)
- 否则进入_EntryList等待
释放锁时:
- _count减1
- 如果_count为0,清空_owner
- 从_EntryList或_WaitSet唤醒等待线程
4. 锁升级全过程解析
4.1 无锁到偏向锁
当第一个线程访问同步块时,JVM会启用偏向锁。这个过程包括:
- 检查Mark Word的偏向锁标志(是否可偏向)
- CAS操作设置线程ID到Mark Word
- 如果成功,线程获得偏向锁
偏向锁的撤销代价很高,需要暂停持有锁的线程(Stop The World)。因此,在已知会有竞争的场景下,可以通过JVM参数关闭偏向锁:
code复制-XX:-UseBiasedLocking
4.2 轻量级锁
当第二个线程尝试获取锁时,偏向锁升级为轻量级锁。加锁过程:
- 在当前线程栈帧中创建Lock Record
- 将Mark Word复制到Lock Record(Displaced Mark Word)
- CAS将Mark Word替换为指向Lock Record的指针
- 如果成功,获取锁;否则自旋重试
轻量级锁适用于线程交替执行的场景。但自旋会消耗CPU,所以JVM会限制自旋次数(默认10次),超过后升级为重量级锁。
4.3 重量级锁
重量级锁通过操作系统的互斥量(mutex)实现,线程竞争失败后会进入阻塞状态。虽然避免了CPU空转,但线程切换的开销很大。
可以通过JVisualVM等工具观察锁竞争情况。如果发现大量线程在BLOCKED状态,就需要考虑优化锁策略了。
5. 内存屏障与happens-before
5.1 内存屏障类型
Java内存模型定义了四种内存屏障:
| 屏障类型 | 作用 | 示例场景 |
|---|---|---|
| LoadLoad | 保证前面的Load先于后面的Load | volatile读后普通读 |
| StoreStore | 保证前面的Store先于后面的Store | volatile写前普通写 |
| LoadStore | 保证前面的Load先于后面的Store | volatile读后普通写 |
| StoreLoad | 保证前面的Store先于后面的Load | volatile写后普通读 |
StoreLoad屏障是最重的,通常对应着锁的释放操作。
5.2 synchronized的屏障插入
在synchronized同步块中,JVM会自动插入内存屏障:
-
进入同步块时:
- LoadLoad屏障:确保同步块内的读操作能看到之前的所有写
- LoadStore屏障:防止读操作与后面的写操作重排序
-
退出同步块时:
- StoreStore屏障:确保同步块内的写操作对其他线程可见
- StoreLoad屏障:防止写操作与后面的读操作重排序
这些屏障保证了synchronized的三大特性:
- 原子性:锁互斥保证
- 可见性:屏障保证写操作刷新到主内存
- 有序性:屏障防止指令重排序
6. 实战经验与性能优化
6.1 锁优化技巧
-
减小锁粒度:将大锁拆分为多个小锁。比如ConcurrentHashMap使用分段锁。
-
锁分离:读写锁分离(ReadWriteLock),适合读多写少场景。
-
锁消除:JIT编译器会分析逃逸对象,去掉不必要的锁。
-
锁粗化:将连续的锁合并,减少锁获取/释放开销。
6.2 常见问题排查
-
死锁:用jstack查看线程dump,分析锁依赖关系。
-
锁竞争:用JMC(Java Mission Control)监控锁竞争情况。
-
性能瓶颈:使用Async Profiler找出热点同步块。
6.3 JDK17的变化
从JDK17开始,偏向锁被彻底移除。主要原因是:
- 现代应用锁竞争更激烈,偏向锁收益下降
- 撤销偏向锁的STW开销大
- 简化JVM锁实现
现在锁升级路径简化为:无锁 → 轻量级锁 → 重量级锁。
7. 从原理到实践
理解synchronized原理后,在实际开发中就能做出更明智的选择:
-
低竞争场景:优先使用synchronized,代码更简洁
-
高竞争场景:考虑ReentrantLock,提供更灵活的控制
-
需要高级功能时(如条件变量、定时锁等):使用Lock API
记住一个原则:不要过早优化。先用synchronized实现正确性,再根据性能测试结果决定是否需要优化。
最后分享一个真实案例:我们曾优化过一个支付系统,通过分析锁竞争情况,将全局锁改为账户粒度的锁,TPS从200提升到了2000。这再次证明,深入理解锁机制能带来巨大的性能提升。
