
👨💻程序员三明治:个人主页 🔥 个人专栏: 《设计模式精解》 《重学数据结构》
🤞先做到 再看见!
synchronize底层使用的是minitor,Monitor 被翻译为监视器,是由jvm提供,c++语言实现。
使用javap -v xxx.class反编译一段代码可以看到机器指令
monitor主要就是跟这个对象产生关联,如下图

Monitor内部具体的存储结构:
具体的流程:
参考回答:
synchronized 在 JVM 中的底层实现原理。这对于理解 Java 并发编程至关重要。synchronized 底层原理(详解版)synchronized 的底层原理可以从三个层面来看:字节码层面、JVM 底层实现 和 硬件层面。我们逐层深入。
当我们使用 synchronized 关键字时,无论是修饰代码块还是方法,在编译后的字节码中都会生成对应的指令。
synchronized(object) { ... },编译器会在同步代码块的前后分别生成 monitorenter 和 monitorexit 指令。public void method() {
synchronized (obj) {
// 同步代码块
System.out.println("hello");
}
}编译后的字节码大致如下:
public void method();
Code:
0: aload_0
1: getfield #2 // 获取对象引用 obj
4: dup
5: astore_1
6: monitorenter // 进入同步块,尝试获取锁
7: getstatic #3 // 获取 System.out
10: ldc #4 // 加载 "hello"
12: invokevirtual #5 // 调用 println
15: aload_1
16: monitorexit // 正常退出同步块,释放锁
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 异常退出同步块,释放锁 (确保在异常情况下也能释放锁)
23: aload_2
24: athrow
25: return关键点:
- 可以看到有两个 `monitorexit` 指令,第一个用于正常退出,第二个用于处理异常情况(隐藏在 `finally` 语义中),这确保了即使同步块内抛出异常,锁也能被正确释放。synchronized 修饰的方法,方法常量池中会设置 ACC_SYNCHRONIZED 标志。public synchronized void method() {
// 方法体
}- 当方法调用时,调用指令(如 `invokevirtual`)会检查这个标志。如果设置了,执行线程会先尝试获取锁(对于实例方法是 `this`,对于静态方法是该类的 `Class` 对象),再执行方法体。方法执行完毕后,无论是正常返回还是异常抛出,都会自动释放锁。小结:从字节码看,synchronized 的实现依赖于 monitorenter 和 monitorexit 这一对指令,或者方法的 ACC_SYNCHRONIZED 标志。
monitorenter 和 monitorexit 指令背后的具体实现,是 JVM 的核心。其关键在于 Java 对象头 和 Monitor。
在 HotSpot 虚拟机中,每个 Java 对象在内存中存储的布局分为三部分:对象头、实例数据、对齐填充。
其中,对象头 是理解锁的关键。它包含两部分信息:
为了在极小的空间内存储尽可能多的信息,Mark Word 被设计成一个非固定的动态数据结构。它会根据对象的状态复用自己的存储空间。下图清晰地展示了 32 位 JVM 下 Mark Word 在不同状态下的结构:
(在 64 位 JVM 下,结构类似,只是空间更大。)
关键点:注意最后 2 位(lock),它标识了对象的锁状态。锁的升级过程就体现在这 2 位的变化上。
JVM 为每个对象都关联了一个内置的 Monitor(管程)。monitorenter 指令的本质就是尝试去获取这个对象对应的 Monitor。
一个 Monitor 由以下部分组成:
null。Blocked 状态的、等待锁的线程队列。当 Owner 释放锁时,JVM 会从 EntryList 中挑选一个线程来成为新的 Owner。Waiting 状态的、调用了 Object.wait() 方法的线程队列。这些线程在等待其他线程的通知(notify/notifyAll)。工作流程:
monitorenter 指令时,会尝试进入(enter)该对象的 Monitor。null,则该线程成功成为 Owner,并将锁的计数器 +1。BLOCKED 状态,直到 Owner 线程释放锁。monitorexit 指令时,锁计数器 -1。当计数器减到 0 时,线程释放 Monitor,不再担任 Owner。然后,EntryList 中的线程会开始竞争锁。在 Java 6 之前,synchronized 是一个重量级锁,性能较差,因为它依赖于操作系统的 Mutex Lock(互斥锁),需要进行用户态到内核态的切换,耗时较长。
为了减少这种性能开销,Java 6 引入了锁升级机制。synchronized 的锁状态从低到高分为四种,升级路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
注意:在 Java 15 之后,偏向锁被标记为废弃并默认禁用,因为维护其带来的收益已不如从前。但理解其原理依然重要。
00,表示轻量级锁状态。BLOCKED 状态。synchronized 的语义保证了原子性、可见性和有序性。
Load Barrier,在同步块结束时加 Store Barrier,强制将工作内存中的修改刷新到主内存,并禁止指令重排序。monitorenter/monitorexit,由 Monitor 保证。对于锁升级过程中的状态变更(如轻量级锁的获取),则是通过 CAS 操作实现的。CAS 是一条 CPU 原子指令(cmpxchg),它保证了“比较-交换”操作的原子性。Monitor实现的锁属于重量级锁,你了解过锁升级吗?



我们可以通过lock的标识,来判断是哪一种锁的等级

每个对象的markword都可以设置monoitor的指针,让对象与monitor产生关联

**加锁的流程 **
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态代表该线程获得了轻量级锁。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。

4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word 恢复成为无锁状态。如果失败则膨胀为重量级锁。

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有.

**加锁的流程 **
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些

解锁流程参考轻量级锁
如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=h70g0sv71wz