1. 为什么我们需要synchronized?
在Java多线程编程中,synchronized关键字就像交通信号灯一样重要。想象一下,如果没有红绿灯,十字路口的车辆会乱成什么样子?线程安全问题就是类似的场景。
我曾在电商项目中遇到过典型的线程安全问题:一个简单的库存扣减操作,在高并发下竟然出现了超卖现象。当时使用简单的if判断库存是否足够,结果多个线程同时通过了检查,导致库存变成了负数。这就是典型的竞态条件(Race Condition)问题。
注意:竞态条件是指多个线程对共享数据同时进行读写操作,最终结果取决于线程执行的精确时序。
synchronized提供了三种基本用法:
- 实例方法同步:锁住当前对象实例
- 静态方法同步:锁住当前类的Class对象
- 代码块同步:可以指定锁对象
java复制// 实例方法同步
public synchronized void method1() {
// 临界区代码
}
// 静态方法同步
public static synchronized void method2() {
// 临界区代码
}
// 代码块同步
public void method3() {
synchronized(this) {
// 临界区代码
}
}
2. synchronized的底层实现原理
2.1 对象头与Mark Word
每个Java对象在内存中都由三部分组成:对象头、实例数据和填充对齐。其中对象头就包含了锁相关的关键信息。
在HotSpot虚拟机中,对象头包含两部分:
- Mark Word:存储对象的运行时数据
- Klass Pointer:指向类元数据的指针
Mark Word在不同锁状态下会存储不同内容:
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | 对象的hashCode、分代年龄等 | 01 |
| 偏向锁 | 持有偏向锁的线程ID、偏向时间戳等 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向互斥量(monitor)的指针 | 10 |
| GC标记 | 空 | 11 |
2.2 Monitor机制
synchronized的底层实现依赖于Monitor对象(也称为管程或监视器锁)。每个Java对象都可以关联一个Monitor对象。
Monitor的工作机制可以类比为医院就诊:
- 患者(线程)到达医院(进入同步代码)
- 先到分诊台(Entry Set)排队
- 被叫号后进入诊室(Owner)
- 如果暂时不能处理(调用wait()),就去候诊区(Wait Set)等待
- 处理完成后离开(退出同步代码)
java复制ObjectMonitor = {
_header = ...; // Mark Word
_count = 0; // 重入次数
_waiters = 0, // 等待线程数
_recursions = 0, // 锁重入次数
_object = ...; // 关联的对象
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 处于wait状态的线程列表
_EntryList = NULL; // 等待锁的线程列表
}
3. 锁的升级与优化
3.1 偏向锁
偏向锁是为了解决"大多数情况下锁不仅不存在竞争,而且总是由同一线程获得"的场景。就像公司门禁卡,大部分时候只有你自己使用。
启用偏向锁后,当线程第一次获得锁时:
- 在对象头和栈帧中记录偏向的线程ID
- 以后该线程进入同步块时,不需要任何同步操作
但一旦出现其他线程尝试获取锁,偏向模式立即结束。在竞争激烈的场景下,偏向锁反而会带来额外的撤销开销。
3.2 轻量级锁
当偏向锁被撤销后,虚拟机会尝试使用轻量级锁。这就像两个人轮流使用会议室,不需要惊动保安(操作系统)。
轻量级锁加锁过程:
- 在栈帧中创建锁记录(Lock Record)
- 将对象头Mark Word复制到锁记录中(Displaced Mark Word)
- 尝试用CAS将对象头替换为指向锁记录的指针
- 如果成功,获得锁;如果失败,说明存在竞争,膨胀为重量级锁
3.3 重量级锁
当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。这就像会议室里突然来了很多人,不得不请保安维持秩序。
重量级锁会导致线程阻塞,涉及用户态到内核态的切换,开销较大。但在高竞争场景下,这是必要的。
4. 常见误区与最佳实践
4.1 错误用法示例
我在代码审查中经常看到这些问题:
- 锁对象选择不当:
java复制// 错误:每次调用都会创建新的字符串对象
public void wrongMethod1(String param) {
synchronized(param) {
// ...
}
}
// 正确:使用final对象作为锁
private final Object lock = new Object();
public void rightMethod() {
synchronized(lock) {
// ...
}
}
- 锁粒度过大:
java复制// 错误:整个方法加锁,性能差
public synchronized void processOrder() {
// 订单处理逻辑(耗时操作)
// 支付处理逻辑
// 库存处理逻辑
}
// 正确:细粒度锁
public void processOrderBetter() {
synchronized(this) {
// 订单处理逻辑
}
// 非同步代码
synchronized(PaymentService.class) {
// 支付处理逻辑
}
// ...
}
4.2 性能优化建议
- 减少锁的持有时间:只在必要的时候加锁,尽快释放
- 减小锁的粒度:从方法锁改为代码块锁
- 避免锁嵌套:容易导致死锁
- 考虑使用读写锁(ReentrantReadWriteLock)替代
- 对于统计计数等场景,考虑使用原子类(AtomicInteger等)
5. 与其他同步机制对比
5.1 synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | JDK实现 |
| 锁获取方式 | 自动获取和释放 | 需要手动lock()和unlock() |
| 可中断性 | 不支持 | 支持lockInterruptibly() |
| 公平锁 | 非公平 | 可配置公平或非公平 |
| 条件变量 | 只能通过wait/notify | 支持多个Condition |
| 性能 | Java 6后优化,性能接近 | 高竞争下表现更好 |
5.2 volatile与synchronized
volatile保证了变量的可见性和有序性,但不保证原子性。它就像公告栏,任何线程都能看到最新内容,但不能防止多人同时修改。
适用场景:
- 状态标志位(如shutdown标志)
- 单例模式的双重检查锁定
- 独立观察结果发布
6. 实战案例分析
6.1 单例模式实现
我见过多种单例实现,但最稳妥的还是枚举方式:
java复制public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}
如果不使用枚举,双重检查锁定需要注意volatile:
java复制public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6.2 线程安全的计数器
在电商秒杀系统中,我们需要线程安全的计数器:
java复制public class SafeCounter {
private int count;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
}
}
public int get() {
synchronized(lock) {
return count;
}
}
}
对于这种简单场景,使用AtomicInteger更高效:
java复制public class BetterCounter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
public int get() {
return count.get();
}
}
7. JVM参数调优建议
针对synchronized性能,可以调整以下JVM参数:
-
偏向锁相关:
- -XX:+UseBiasedLocking:启用偏向锁(JDK15后默认禁用)
- -XX:BiasedLockingStartupDelay=0:JVM启动后立即启用偏向锁
-
自旋锁相关:
- -XX:+UseSpinning:启用自旋(现代JVM默认启用)
- -XX:PreBlockSpin=10:自旋次数默认值
-
锁膨胀相关:
- -XX:+UseHeavyMonitors:强制使用重量级锁
- -XX:+PrintBiasedLockingStatistics:打印偏向锁统计信息
在实际项目中,我发现过早优化往往是性能问题的根源。建议先确保正确性,再通过性能测试找出真正的瓶颈。
