首页>>后端>>java->Synchronized原理分析

Synchronized原理分析

时间:2023-11-30 本站 点击:1

Synchronized是基于JVM来实现的,通过基于JVM进入和退出Monitor对象来实现方法同步和代码块同步。方法同步和代码块同步的实现细节是不一样的,代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是通过常量池中的ACC_SYNCHRONIZED标识符,首先会检查方法的ACC_SYNCHRONIZED方法标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体。

Synchronized锁,其本质就是锁的对象,所以了解原理之前先介绍下java对象。

java对象

对象是存储在堆中的,每个对象都会包含对象头,实例属性,和填充数据(如果是数组对象还会多一个记录数组长度的属性)填充数据是啥呢,这是因为虚拟机要求对象的大小至少是8字节的整数倍,所以如果对象的大小达不到这个条件,就会通过填充数据来满足要求。

我们来通过代码具体看下这个java对象

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version></dependency>创建一个对象并进行测试publicclassHeadObject{privatebooleanflag;}publicclassTestDemo{privatestaticHeadObjectheadObject=newHeadObject();publicstaticvoidmain(String[]args){System.out.println(ClassLayout.parseInstance(headObject).toPrintable());}}

对象内部构造需要借助一个jar包才能展现出来,来看下执行结果

通过输出结果可以看到,对象头占了12个字节,属性占了1个字节,加起来13个,不是8的倍数,所以会加大小为3个字节的填充数据。

如果我们把HeadObject中的字段flag改为int类型,看下有啥变化

可以看到填充数据就没了,因为对象头加属性大小是16字节,正好是8的倍数,因此不需要进行数据填充。

对象头

synchronized锁真正完成锁对象的逻辑就在对象头里

对象头一般采用两个属性来存储对象头,其主要结构就是Mark Word 和Class Metadata Address组成。

Mark Word里默认存储了对象的hashcode,分代年龄和锁标记位,这里来看下32位JVM的Mark Word默认存储结构

Mark Word内存存储的内容会随着锁状态的不同而有所改变。

重量级锁实现原理

每个在jdk1.6之前,Synchronized锁实现的都是重量级锁,锁的标识位是10,其中指针指向的是monitor对象(监视器锁)的起始位置,每个对象都有一个与之关联的Monitor对象,当Monirot被某个线程持有后,它便处于锁定状态。在JAVA虚拟机中,montitor是由ObjectMonitor实现,其主要数据结构如下图所示:

ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_owner区域并把monitor的owner设置为当前线程,同时count加1,若线程调用了wait方法,将释放当前持有的monitor,owner变量恢复为null,count减1,同时该线程会进入WaitSet集合中等待被唤醒,若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取锁。

重量级锁的缺点也很明显

依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。

研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验,找了一些典型的软件,测试同一个线程加锁解锁的重复率,如下图所示,可以看到重复加锁比例非常高。早期JVM 有 19% 的执行时间浪费在锁上

在jdk1.6开始,Synchronized加入了偏向锁和轻量级锁来提升了Synchronized的整体性能。

无锁到偏向锁

在大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得,因此为了减少同一线程获取锁的代价引入了偏向锁。

首先检测是否为可偏向的状态,如果处于可偏向的状态,会测试当前Mark Word的线程ID是否指向自己,如果是,不需要再次获取锁,直接执行同步代码。

如果线程ID不是自己的线程ID,就会通过CAS获取锁,获取成功代表当前偏向锁不存在竞争,获取失败,这说明当前偏向锁存在锁竞争,这时候就会先启动偏向锁撤销

锁撤销流程:当出现竞争,持有锁的线程会等待一个全局安全点阻塞,遍历线程栈,查看是否有被锁对象的锁记录(Lock Record),如果有Lock Record,需要修复锁记录和Mark Word,使其变成无锁状态,将是否为偏向锁状态置为0,然后开始进行轻量级锁加锁流程。

偏向锁升级为轻量级锁

接着上面继续讲解,怎么升级为轻量级锁呢

首先线程在自己的栈帧中创建锁记录

线程将Mark Word拷贝到线程栈的Lock Record中

将锁记录中的Owner指针指向加锁对象

将锁对象的对象头的MarkWord替换为指向锁记录的指针

这时锁标志位变成00,表示轻量级锁

轻量级锁升级成重量级锁

当锁升级为轻量级锁后,如果依然有新的线程来竞争锁,首先这个新线程会自旋尝试获取锁,尝试到一定次数(默认10次)如果依然没有拿到锁,锁就会升级为重量级锁。

这里解释一下,为什么会自旋一段时间再转为重量级锁。

一般来说,同步代码块中的代码应该会很快执行结束,这时候新的线程自旋一段时间会很容易拿到锁,如果没拿到,因为自旋是个死循环,耗CPU,所以会设置自旋次数,然后转为重量级锁,这些竞争锁的线程就不必一直自旋了。

同步代码块和同步方法底层代码

通过这个底层代码,再结合讲解的Synchronized底层原理,就会更清晰的了解Synchronized的工作流程。

对于同步方法来说,它是通过ACC_SYNCHRONIZED访问标志位来区分方法是否是同步方法。当方法调用的时候,首先会检查ACC_SYNCHORONIZED方法标志是否被设置,如果设置了,则需要先获得monitor,然后再执行方法。

而同步代码块,是通过monitorenter和monitorexit指令来实现,为了保证方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行编译器会自动产生一个异常处理器,这个异常处理器会来执行这个monitorexit指令。

如果觉得小蛋写的还不错,就点赞关注一波吧,一起在这周遭的社会展现我们的坚持


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/4946.html