核心目标: 保证同一时间,只有一个线程能执行被synchronized
保护的代码块或方法。
实现原理的核心:每个Java对象都自带一把"锁"和一个"监视器"(Monitor)。
1. 锁的载体:对象头 (Object Header)
-
想象: 每个Java对象(比如
new Object()
)在内存中除了存你的数据,还有一个"对象头",就像它的身份证和门禁卡。 -
关键部分 - Mark Word: 对象头里最重要的部分叫
Mark Word
。它很小(通常32位或64位),但非常灵活,用来存储:- 对象的哈希码 (HashCode)
- 垃圾回收的分代年龄 (GC Age)
- 锁状态标志位: 这是核心!它标记这个对象当前处于哪种锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
- 指向重量级锁的指针: 当锁升级到重量级锁时,这里存储指向一个真正"锁对象"(Monitor)的地址。
- 偏向线程ID / 轻量级锁指针: 在偏向锁或轻量级锁状态下,存储相关线程或栈帧的信息。
2. 锁的守护者:Monitor (监视器锁)
-
想象: 当锁升级到"重量级锁"状态时,JVM会为这个对象关联一个真正的
Monitor
对象(也叫管程或监视器锁)。你可以把它想象成一个特殊的小房间(临界区)的管理员。 -
Monitor 的结构:
- Owner (持有者): 记录当前哪个线程持有这把锁(正在房间里执行代码)。初始为
null
。 - EntryList (入口队列/竞争队列): 当线程A持有锁时,线程B、C也想来获取锁,但它们发现Owner是A(房间有人),于是它们就在这个队列里阻塞等待。这是一个互斥队列。
- WaitSet (等待集合): 如果持有锁的线程A自己主动调用
wait()
方法释放锁并等待,那么线程A就会进入这个WaitSet集合中休眠等待 ,直到其他线程调用notify()
/notifyAll()
唤醒它。这是一个协作队列。
- Owner (持有者): 记录当前哪个线程持有这把锁(正在房间里执行代码)。初始为
3. 锁的升级过程 (锁膨胀 - 优化关键!)
Java为了在不同竞争场景下平衡性能和开销,设计了锁升级机制:
-
无锁 (New / Normal):
- 对象刚创建时,或者锁被释放后。
- Mark Word里没有锁信息(或标志位是无锁)。
- 多个线程可以自由竞争。
-
偏向锁 (Biased Locking):
-
场景: 绝大多数情况下,锁总是被同一个线程多次访问(比如一个线程循环调用某个同步方法)。竞争极少。
-
操作:
- 当第一个线程Thread-A访问同步块时,JVM通过CAS操作 (Compare-And-Swap,一种CPU原子指令)尝试将Mark Word中的锁标志位设置为"偏向锁",并把自己的线程ID记录到Mark Word中。
- 成功: Thread-A以后每次进入这个同步块,只需要检查Mark Word里的线程ID是不是自己。如果是,直接执行!开销极小(就一次内存地址比较),无需任何原子操作。
-
优势: 没有竞争时,性能接近无锁!
-
失效: 一旦有另一个线程Thread-B尝试获取锁(意味着有竞争了),偏向锁就会撤销。Thread-B会等待Thread-A到达全局安全点(如GC点)暂停,然后检查Thread-A是否还活着或已退出同步块。
- 如果Thread-A已退出或不活动,撤销偏向锁,恢复到无锁状态,Thread-B可以尝试获取(可能直接升级为轻量级锁)。
- 如果Thread-A还在活动,则升级为轻量级锁。
-
-
轻量级锁 (Lightweight Lock / 自旋锁):
-
场景: 存在少量、短时间的竞争。线程交替执行同步块,不太会长时间阻塞。
-
操作:
-
线程在进入同步块前,JVM会在当前线程的栈帧 中创建一个叫
Lock Record
的空间,用于保存锁对象原始的Mark Word拷贝 (Displaced Mark Word)。 -
然后,线程使用CAS操作 尝试将锁对象的Mark Word更新为指向自己栈帧中
Lock Record
的指针。 -
成功: 锁标志位变为"轻量级锁",线程获取锁成功。
-
失败: 两种情况:
- 锁重入: 如果发现锁对象的Mark Word指向的就是自己栈帧中的
Lock Record
,说明是同一个线程再次进入(可重入),直接执行。 - 存在竞争: 其他线程占用了锁。此时,当前线程不会立即阻塞,而是采用自旋 策略(在CPU上空循环等待一小段时间),不断尝试CAS操作去获取锁。自旋避免了昂贵的线程阻塞和唤醒操作。
- 锁重入: 如果发现锁对象的Mark Word指向的就是自己栈帧中的
-
-
优势: 在低竞争下,避免了OS层面的线程切换开销。
-
升级: 如果自旋达到一定次数(JVM自适应策略决定)或者自旋期间又来了第三个线程竞争,轻量级锁就会升级为重量级锁。
-
-
重量级锁 (Heavyweight Lock / 互斥锁):
-
场景: 存在激烈、长时间的竞争。
-
操作:
-
锁标志位变为"重量级锁"。
-
Mark Word中存储指向
Monitor
对象(前面提到的那个管理员)的指针。 -
未获取到锁的线程,不再自旋,而是被JVM通过操作系统内核调用(如
pthread_mutex_lock
)挂起(阻塞) ,放入Monitor
的EntryList
队列中等待。 -
当持有锁的线程退出同步块,释放锁时:
- 它会唤醒
EntryList
队列中的一个或所有线程(取决于策略)。 - 被唤醒的线程会重新竞争锁(可能又失败,继续阻塞)。
- 它会唤醒
-
-
开销: 涉及用户态到内核态的切换,线程阻塞和唤醒代价非常高昂。
-
优势: 在激烈竞争下,避免了CPU空转(自旋)浪费资源,让出CPU给其他线程用。
-
4. synchronized
在代码中的表现
-
同步代码块 (
synchronized(obj) {...}
):- 锁对象就是你显式指定的
obj
。 - JVM编译后在代码块开始处插入
monitorenter
指令,在结束处和异常路径处插入monitorexit
指令。 - 执行
monitorenter
指令时,JVM会根据锁对象的状态(无锁/偏向/轻量级/重量级)执行上面描述的相应锁获取/升级逻辑。 - 执行
monitorexit
指令时,执行锁释放逻辑(释放锁、唤醒等待线程、可能降级锁状态)。
- 锁对象就是你显式指定的
-
同步实例方法 (
public synchronized void method()
):- 锁对象就是该方法所属的当前对象实例 (
this
) 。 - 方法调用时,JVM会检查方法的访问标志
ACC_SYNCHRONIZED
。有这个标志的方法,其调用和返回在JVM内部会被隐式地加上monitorenter
和monitorexit
的效果,锁对象就是this
。
- 锁对象就是该方法所属的当前对象实例 (
-
同步静态方法 (
public static synchronized void method()
):- 锁对象是该方法所属类的
Class
对象 (如MyClass.class
)。 - 同样通过
ACC_SYNCHRONIZED
标志实现,锁对象是当前类的Class
对象。
- 锁对象是该方法所属类的
5. 关键特性与要点
- 可重入性 (Reentrancy): 同一个线程可以多次 获取同一个锁对象上的锁。例如,在一个
synchronized
方法里调用另一个synchronized
方法(锁对象相同)是允许的。JVM通过记录锁的持有计数来实现。这是synchronized
的基础特性,非常重要。 - 自动释放: 无论同步块是正常退出还是因为异常退出,JVM都会保证锁被释放。这是由编译器插入的
monitorexit
指令(包括在异常处理表里)保证的。 - 内存可见性: 获得锁不仅意味着独占执行权,还意味着线程会刷新工作内存并从主内存重新加载共享变量 ,保证在锁保护区内看到的变量是最新的。释放锁时,会把修改刷新回主内存。这解决了线程间的可见性问题。
- 锁的是对象,不是代码: 一定要牢记,锁是绑定在对象上的。两个线程锁不同的对象,不会互相阻塞。锁同一个对象才会互斥。
总结流程图
text
rust
线程尝试进入synchronized块
|
V
检查对象锁状态 (Mark Word)
|
|--- 无锁? ---> CAS尝试获取偏向锁(记录线程ID) ---成功---> 执行代码 (下次进来只需检查ID)
| |
| ---失败(竞争)---> 升级轻量级锁
|
|--- 偏向锁? ---> 检查线程ID是否是自己?
| |
| |--- 是 ---> 执行代码 (重入计数+1)
| |
| --- 否 ---> 撤销偏向锁 (暂停原线程) ---> 升级轻量级锁
|
|--- 轻量级锁? ---> 检查锁记录指针是否指向自己栈帧?
| |
| |--- 是 (重入) ---> 执行代码 (重入计数+1)
| |
| --- 否 ---> CAS尝试将Mark Word指向自己的锁记录
| |
| |--- 成功 ---> 执行代码
| |
| --- 失败 ---> 自旋等待 (循环尝试CAS)
| |
| |--- 成功 ---> 执行代码
| |
| --- 自旋超限或有新竞争者 ---> 升级重量级锁
|
|--- 重量级锁? ---> 检查Monitor的Owner是否是自己?
|
|--- 是 (重入) ---> 执行代码 (重入计数+1)
|
--- 否 ---> 进入EntryList队列阻塞等待 (OS挂起线程)
|
|--- 被唤醒 ---> 竞争锁 (可能失败,继续阻塞)
|
--- 竞争成功 ---> 成为Owner,执行代码
退出synchronized块:
重入计数-1
如果计数 > 0: 直接返回 (锁还没完全释放)
如果计数 = 0:
轻量级锁: CAS将Displaced Mark Word拷贝回对象头 (恢复状态)
重量级锁: 设置Owner为null,唤醒EntryList中的一个/所有等待线程
无操作 (偏向锁在撤销时已处理)
简单记忆:
- 偏向锁: 贴个名字标签(线程ID),熟人(同一线程)来了直接进。
- 轻量级锁: 门口挂个小牌子(指向线程栈的指针),大家(少量线程)在门口稍微等一下(自旋),谁先抢到牌子(CAS成功)谁进。
- 重量级锁: 门口排长队(EntryList),有管理员(Monitor),里面的人出来管理员才叫下一个进去(线程阻塞唤醒)。
理解了这个锁升级过程和Monitor的概念,就掌握了synchronized
的核心原理。它通过这种分层优化的策略,在无竞争、低竞争和高竞争的不同场景下,都尽可能达到最好的性能。