一、synchronized 的基本概念

1.1 作用

synchronized 是 Java 中的关键字,用于实现线程同步,确保:

  • 原子性:确保多个操作要么全部执行,要么全部不执行

  • 可见性:确保一个线程修改共享变量后,其他线程能立即看到

  • 有序性:防止指令重排序

1.2 使用方式

// 1. 同步实例方法
public synchronized void instanceMethod() {
    // 锁是当前实例对象
}

// 2. 同步静态方法
public static synchronized void staticMethod() {
    // 锁是当前类的Class对象
}

// 3. 同步代码块
public void method() {
    synchronized (lockObject) {
        // 锁是指定的对象
    }
}

二、底层实现原理

2.1 Java 对象头

每个 Java 对象在内存中都包含对象头,其中与锁相关的信息存储在 Mark Word 中:

32位 JVM 对象头布局:

|--------------------------------------------------------------|
|                     Mark Word (32 bits)                      |
|--------------------------------------------------------------|
|      锁状态      | 25 bits           | 4 bits | 1 bit | 2 bits |
|-----------------|-------------------|--------|-------|--------|
| 无锁             | hashCode          | 分代年龄 | 0     | 01     |
| 偏向锁           | ThreadID | Epoch | 分代年龄 | 1     | 01     |
| 轻量级锁         | 指向栈中锁记录的指针           |        | 00     |
| 重量级锁         | 指向重量级锁(Monitor)的指针   |        | 10     |
| GC标记          | 空                 |        |       | 11     |

64位 JVM 对象头布局类似,只是位数不同。

2.2 Monitor 机制

synchronized 是通过 Monitor(监视器锁)实现的:

// 字节码层面
public void synchronizedMethod() {
    synchronized (this) {
        // 业务代码
    }
}

// 对应的字节码:
0: aload_0
1: dup
2: astore_1
3: monitorenter    // 进入同步块
4: aload_1
5: monitorexit     // 正常退出
6: goto 14
9: astore_2
10: aload_1
11: monitorexit    // 异常退出
12: aload_2
13: athrow
14: return

三、锁升级过程(锁膨胀)

Java 6 之后,synchronized 实现了锁升级机制,以提高性能:

3.1 无锁状态

初始状态,对象未被任何线程锁定。

3.2 偏向锁(Biased Locking)

适用场景: 只有一个线程访问同步块

流程:

  1. 线程第一次访问同步块时,检查 Mark Word 中的锁标志位

  2. 如果是 01(无锁/偏向锁),通过 CAS 将线程 ID 写入 Mark Word

  3. 成功则获取偏向锁,锁标志位不变(仍是 01)

  4. 后续同一线程进入时,只需检查线程 ID 是否匹配

优点: 只有一个线程时,性能接近无锁

3.3 轻量级锁(Lightweight Locking)

触发条件: 有多个线程竞争,但竞争不激烈

流程:

  1. 当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁

  2. 在当前线程的栈帧中创建锁记录(Lock Record)

  3. 将对象头 Mark Word 复制到锁记录中(Displaced Mark Word)

  4. 尝试通过 CAS 将对象头替换为指向锁记录的指针

  5. 成功则获得锁,失败则自旋重试

自旋策略:

  • 自旋次数有限(默认 10 次,可通过参数调整)

  • 自适应自旋:根据上次自旋成功与否动态调整

3.4 重量级锁(Heavyweight Locking)

触发条件: 竞争激烈或自旋失败

流程:

  1. 轻量级锁升级为重量级锁

  2. 对象头指向操作系统层面的互斥量(Mutex)

  3. 未获取锁的线程进入等待队列,被操作系统挂起

  4. 需要用户态到内核态的切换,开销大

四、锁的优缺点对比

锁类型 优点 缺点 适用场景
偏向锁 加锁解锁无额外消耗 有竞争时会额外消耗 单线程访问
轻量级锁 竞争线程不阻塞,自旋 自旋消耗CPU 追求响应时间,同步块执行快
重量级锁 竞争线程不消耗CPU 线程阻塞,响应慢 追求吞吐量,同步块执行时间长

五、锁优化技术

5.1 锁消除(Lock Elimination)

public String concatString(String s1, String s2, String s3) {
    // StringBuffer 是线程安全的,但这里 sb 是局部变量
    // JVM 检测到 sb 不可能逃逸出方法,会消除锁
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

5.2 锁粗化(Lock Coarsening)

// 多次加锁解锁合并为一次
for (int i = 0; i < 1000; i++) {
    synchronized (this) {  // JVM 可能会合并这1000次锁操作
        // do something
    }
}
// 可能优化为:
synchronized (this) {
    for (int i = 0; i < 1000; i++) {
        // do something
    }
}

5.3 自适应自旋(Adaptive Spinning)

JVM 根据之前的自旋成功率动态调整自旋次数。

六、与 ReentrantLock 对比

特性 synchronized ReentrantLock
实现方式 JVM 内置 Java API 实现
锁获取 自动获取释放 手动 lock/unlock
可中断 不支持 支持
公平锁 非公平 可选公平/非公平
条件变量 只能有一个 可创建多个 Condition
性能 Java 6 后优化,两者接近 功能更丰富

七、使用建议

  1. 优先使用 synchronized:在 Java 6+ 中性能已大幅优化

  2. 减少锁粒度:使用细粒度锁,减少竞争

  3. 缩短持有时间:同步块内代码尽可能少

  4. 避免嵌套锁:防止死锁

  5. 考虑读写分离:读多写少场景使用 ReadWriteLock

八、常见问题

Q1: synchronized 能否重入?

可以。 同一线程可重复获取同一把锁。

Q2: synchronized 是否公平?

默认非公平。 但有一定偏向:进入等待队列前会尝试直接获取锁。

Q3: synchronized 能否中断?

不能。 线程在等待锁时不能被中断。

Q4: synchronized 锁的是什么?

  • 实例方法:锁的是当前对象实例(this)

  • 静态方法:锁的是当前类的 Class 对象

  • 同步块:锁的是括号里的对象

总结

  synchronized 是 Java 并发编程的基石,通过对象头、Monitor 机制和锁升级策略,在保证线程安全的同时,兼顾了性能。理解其底层原理有助于编写更高效、更安全的并发程序。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐