Synchronized 原理详解
一、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)
适用场景: 只有一个线程访问同步块
流程:
-
线程第一次访问同步块时,检查 Mark Word 中的锁标志位
-
如果是 01(无锁/偏向锁),通过 CAS 将线程 ID 写入 Mark Word
-
成功则获取偏向锁,锁标志位不变(仍是 01)
-
后续同一线程进入时,只需检查线程 ID 是否匹配
优点: 只有一个线程时,性能接近无锁
3.3 轻量级锁(Lightweight Locking)
触发条件: 有多个线程竞争,但竞争不激烈
流程:
-
当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁
-
在当前线程的栈帧中创建锁记录(Lock Record)
-
将对象头 Mark Word 复制到锁记录中(Displaced Mark Word)
-
尝试通过 CAS 将对象头替换为指向锁记录的指针
-
成功则获得锁,失败则自旋重试
自旋策略:
-
自旋次数有限(默认 10 次,可通过参数调整)
-
自适应自旋:根据上次自旋成功与否动态调整
3.4 重量级锁(Heavyweight Locking)
触发条件: 竞争激烈或自旋失败
流程:
-
轻量级锁升级为重量级锁
-
对象头指向操作系统层面的互斥量(Mutex)
-
未获取锁的线程进入等待队列,被操作系统挂起
-
需要用户态到内核态的切换,开销大
四、锁的优缺点对比
| 锁类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁解锁无额外消耗 | 有竞争时会额外消耗 | 单线程访问 |
| 轻量级锁 | 竞争线程不阻塞,自旋 | 自旋消耗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 后优化,两者接近 | 功能更丰富 |
七、使用建议
-
优先使用 synchronized:在 Java 6+ 中性能已大幅优化
-
减少锁粒度:使用细粒度锁,减少竞争
-
缩短持有时间:同步块内代码尽可能少
-
避免嵌套锁:防止死锁
-
考虑读写分离:读多写少场景使用 ReadWriteLock
八、常见问题
Q1: synchronized 能否重入?
可以。 同一线程可重复获取同一把锁。
Q2: synchronized 是否公平?
默认非公平。 但有一定偏向:进入等待队列前会尝试直接获取锁。
Q3: synchronized 能否中断?
不能。 线程在等待锁时不能被中断。
Q4: synchronized 锁的是什么?
-
实例方法:锁的是当前对象实例(this)
-
静态方法:锁的是当前类的 Class 对象
-
同步块:锁的是括号里的对象
总结
synchronized 是 Java 并发编程的基石,通过对象头、Monitor 机制和锁升级策略,在保证线程安全的同时,兼顾了性能。理解其底层原理有助于编写更高效、更安全的并发程序。
更多推荐



所有评论(0)