synchronized的优化
Java对象头
Java对象头是HotSpot虚拟机中每个对象在内存中都有的元数据结构。对象头的组成主要分为以下几个部分:
组成部分 | 位数(32位JVM) | 位数(64位JVM) | 描述 |
---|---|---|---|
Mark Word | 32位 | 64位 | 用于存储对象的运行时数据,如哈希码、GC状态标志、锁信息等。 |
Klass Pointer | 32位 | 64位(压缩指针为32位) | 指向对象的类元数据的指针,表示这个对象是哪个类的实例。 |
对齐填充(Padding) | 可选 | 可选 | 为了保证对象大小是8字节的倍数,可能会添加填充字节。 |
MarkWord的结构
32位:
64位:
Monitor的结构
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
- 刚开始 Monitor 中 Owner 为 null,说明没有人抢锁
- 当 Thread-1 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-1,Monitor中只能有一个 Owner
- 在 Thread-1 上锁的过程中,如果 Thread-2,Thread-3,Thread-4 也来执行 synchronized(obj),就会进入EntryList BLOCKED
- Thread-1 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-5,Thread-6 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,需要通过notify、notifyAll来唤醒。
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
代码同步块是通过使用monitorenter
、monitoreexit
指令实现的,当线程执行到monitorenter指令的时候,将会尝试获取对象所对应的monitor的使用权,也就是获取到对象的锁。
锁的升级与对比
在Java6中为了减少获取锁和释放锁带来的性能损耗,引入了“偏向锁”和“轻量锁”。
无锁状态、偏向锁、轻量级锁、重量级锁。
锁可以升级,但是不能降级。
轻量级锁
场景:有一个方法或对象虽然是多线程的,但是岔开时间使用的。
- 创建锁记录(Lock Record),每一个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中的Object reference指向锁对象,并且尝试用cas替换Object的mark word,把mark word的内容写入锁中
- 如果 cas 替换成功,对象头中存储了
锁记录地址和状态 00
,表示由该线程给对象加锁,这时图示如下
-
如果cas失败了,有两种情况
- 如果是其他线程线程想要来抢锁,就会进入锁膨胀的过程,变为重量级的锁,会为锁对象关联一个monitor对象,争夺monitor的所属权
- 如果是自己这个线程的锁重入,那就再加一条Lock Record,重入计数+1。
-
当退出
synchronized
代码块的时候,如果有取值为null的锁记录,表示有重入,重制锁记录,重入计数减1。
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
轻量级锁膨胀为重量级锁
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,Thread -1尝试自旋转获取锁,如果没有获取到,进入锁膨胀状态,升级为重量级锁。
- 然后把自己加入到monitor中的EntryList中进行等待获取锁。
- Thread -0结束后,会尝试Cas替换Object中的Mark Word,这个时候会发现替换失败,Thread -0就会释放锁,并且通知所有线程可以来抢锁了。
(竞争重量级锁时的)自旋优化
自旋成功:
自旋失败:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
偏向锁
在大多数情况下,锁不仅不存在多线程之间的竞争,而且还总是由同一个线程多次获取到锁,所以,为了让线程获取锁的代价更低就引入了偏向锁。当一个线程访问同步块并获取到锁的时候,会在对象头和栈帧中的锁记录里存放锁偏向的线程ID,以后这个线程在进入、退出同步块的时候就不需要进行CAS操作来加锁和解锁吗,只需要看对象头中的Mark Word中是否存储这指向当前线程的偏向锁。
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
调用HashCode之后就不会再进入偏向锁状态,因为没地方存HashCode。
总结
如果偏向锁是开启的,并且没有延迟,有延迟的不会刚开始就创建偏向锁
刚开始从无锁状态—>偏向锁的状态
然后如果有有人与偏向锁的持有者进行竞争就会升级会轻量锁
然后从偏向锁状态—>轻量锁
如果继续有人和轻量锁的持有者竞争,一开始会尝试自旋获取锁,因为重量级锁对性能的损耗还是比较大。
如果自旋过程中获取到了锁,就不会升级为重量级的锁
如果自选过程中没有获取到锁,就会升级会重量级别的锁