Synchronized的原理和升级过程
参考文献:
Java精通并发-通过openjdk源码分析ObjectMonitor底层实现 - cexo - 博客园 (cnblogs.com)
由Java 15废弃偏向锁,谈谈Java Synchronized 的锁机制 - 云+社区 - 腾讯云 (tencent.com)
15.多线程编程中锁的4种状态-无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态_郑学炜的技术博客-CSDN博客
ps:这是对知识点的归纳和自己的一些理解
如果有说的不对的地方,欢迎在评论去指出(侵删)
1、对象布局
首先,我们要知道对象在内存中的布局:
已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。
- 对象头由MarkWord和**Klass Point(**类型指针)组成
- Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例(一般是4个字节)
- Mark Word用于存储HashCode、分代年龄、锁标记位、threadID等信息。(一般是8个bytes)
- 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
- 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的


查看对象在内存中的布局和样子:
public class Test2 {
private static class T{
int i;
long l;
boolean b;
String s = "Hello world";
}
public static void main(String[] args) throws InterruptedException {
T t = new T();
//这个方法需要下载一个openjdk的包
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}

2、Synchronized的原理
通过第一部分可以知道,Synchronized不论是修饰方法还是代码块,都是通过持有修饰锁对象来实现同步,那么Synchronized锁对象是如何实现上锁的呢?
答案是:经过synchronized的代码,对其锁对象的markword进行了修改,这个修改其实就是使其中的锁信息的指针指向monitor(一般讨论的是重量级锁,偏向锁和轻量级锁有其他方式完成,在下节会说到)
jvm中的同步是基于进入与退出监视器对象(monitor,也叫管程对象)来实现,每一个对象实例都会有一个monitor对象,monitor对象会和java对象一同创建并和一同销毁,monitor对象是有c++来实现的
当多个线程同时访问一段同步代码时,这些线程会被放到一个EntrySet集合中,处理阻塞状态的线程都会被放到该列表当中。接下来当线程获取到对象的monitor时,monitor对象中的_owner属性指向持有monitor的线程,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有mutex,这时其他线程就无法再获取到该mutex
对象头----> monitor ----操作系统层面----> mutexlock锁
下面是c++实现的monitor对象:
如果线程调用了wait()方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入WaitSet集合(等待集合)中,等待下次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它会释放掉所持有的mutex。
总结一下:同步锁再这种实现方式中,因为monitor依赖于底层的操作系统实现,这样就存在用户态和内核态之间的切换,所以会增加开销
证明:
public class Test2 { private static class T{} public static void main(String[] args) throws InterruptedException { T t = new T(); System.out.println(ClassLayout.parseInstance(t).toPrintable()); //t为锁对象,两次markword不一样 synchronized (t){ System.out.println(ClassLayout.parseInstance(t).toPrintable()); } } }
synchronized的几个状态和升级的锁的类型(即markword中的锁信息的内容):
在jvm指令的层次上讲(成对出现):
方法块方式:
monitorenter : 加锁,进入monitor对象
monitorexit :解锁,退出monitor对象
对于monitorenter:
- 每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:
- 如果monitor的进入数(count)为0,则该线程可以进入monitor,并将monitor进入数(count)设置为1,该线程即为monitor的拥有者。
- 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数(count)加1,所以synchronized关键字实现的锁是可重入的锁。
- 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数(count)为0,再重新尝试获取monitor。
对于monitorexit:
只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数(count)减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
修饰符方式
ACC_SYNCHRONIZED标志设置
本质也是对于对象监视器 monitor 的获取与释放
3、锁升级
为什么要锁升级?
阻塞或唤醒一个Java线程需要操作系统切换CPU状态(用户态转到内核态)来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长
jdk1.6之后添加了很多细节,其中就有对于锁的:
Synchronized的升级优化的过程:
- 无锁
- 首先有一个线程到来,先尝试着给这个线程加一个偏向锁
- 如果有另外一个线程来竞争,那么这个锁就会升级为轻量级锁(多数情况下是自旋锁)
- 并发数越来越高,一个线程自旋了很长时间(一般是10次)还没拿到锁,就再次升级为重量级锁(悲观锁)
原理:
synchronized 锁升级原理:
在锁对象的对象头里面有一个 threadid 字段,thread1访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为当前线程 id
线程2到来的时候会先判断 threadid 是否和thread1的 一致,如果一致则可以直接使用对象锁
如果不一致(表示线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID)
- 查看java对象头中记录的treadid是否存活,如果没有存活,那么锁对象先被重置为无锁状态,则线程2将其再次变为偏向锁,随后存储线程2的id为threadid
- 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁(通过自旋持续尝试获取锁对象),第一次升级会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,使得markword中的轻量级锁的指针指向这块空间
自旋一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
(必看)----------------------------------------------------------
-
首先对象头中的markword信息:

-
偏向锁
偏向锁加锁过程:
当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 判断是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且将threadid设置为当前线程 ID,表示进入偏向锁状态。
一旦出现其它线程竞争锁资源,偏向锁就会被撤销。撤销时机是在
全局安全点,暂停持有该锁的线程,同时检查该线程是否存活。如果存活,则撤销偏向锁升级为轻量级锁;如果死亡则先将锁对象设置为无锁,在由新线程升级为偏向锁为什么要加偏向锁?(偏向锁解决什么问题?)
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,减少锁竞争的资源开销,才引入的偏向锁。
jdk15偏向锁为什么被废除?缺点是什么?
偏向锁的缺点:在
高并发场景下,大量线程同时竞争同一个锁资源,偏向锁会被撤销,发生stop the world后,开启偏向锁会带来更大的性能开销(这就是 Java 15 取消和禁用偏向锁的原因) -
轻量级
轻量级锁的上锁过程
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示。

(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果自旋多次更新成功,则执行步骤(4),否则执行步骤(5)。

(4)**如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,**即表示此对象处于轻量级锁定状态.
(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,因为时间也不长,倒不如让其自旋一会,追求速度
-
重量级锁
如果自旋的时间太长也不行,因为自旋是要消耗CPU的,自旋的次数是有限制的,重量级锁把除了拥有锁的线程都阻塞,防止CPU空转消耗cpu资源。
重量级锁通过对象内部的监视器(monitor)实现,需要从用户态切换到内核态,切换成本非常高。
当持有锁的时间够长时,可以使用重量级锁。
总结图

注意:锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。




273

被折叠的 条评论
为什么被折叠?



