Synchronized锁升级的过程
无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁
早期synchronized效率低的原因
- Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源
- 在Java早期版本中,synchronized属于重量级,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex
Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高
Java对象头
- Java对象的结构由三部分组成。对象头,实例数据(略)以及对齐填充(略)
对象头
- 普通对象的对象头由两部分组成 : Mark Word Klass Word
- 数组对象的对象头由三部分组成 : Mark Word Klass Word Array Length
- Klass Word 可以理解为一个指针,通过这个指针可以定位这个对象所属的类型
- Array Length 由数组对象特有,是用来标识数组的长度的结构
由于java中在使用Synchronized的时候与锁相关的基本都和对象头中的MarkWord有关,所以接下来我们针对java的对象头中的MarkWord进行详细说明
- Synchronized中的锁直接依赖于对象头中的MarkWord。然后MarkWord要么通过markword的锁的标志位(状态位)直接确定,要么根据指向栈帧的锁记录来实现,要么根据指向操作系统的mutex
lock来进一步实现,不过有一点可以肯定的是,无论从用户态或者内核态的方式,最终都会在markword中回写锁的标志位
Monitor
- Monitor (管程或者监视器)
是一种用于实现并发控制的同步机制,是一种高级抽象的同步原语。提供了一种结构化的方法来管理共享资源的并发访问。
管程的实现可以基于互斥锁(Mutex)和条件变量(Condition
Variable),通过对互斥锁的加锁和解锁来实现对管程的进入和退出操作,通过条件变量的等待和通知操作来实现线程的等待和唤醒。 - 一个关键的理解点 :因为每一个java对象都有一个monitor监视器 , 因此 Java 中的每一个对象都可以作为一把锁
。Monitor的本质是依赖于底层操作系统的Mutex Lock实现
Monitor对象的结构
Monitor对象由三部分组成
一、Owner
- Owner: 它标识了当前的持有Monitor对象的线程信息
- 如果某个线程成为了Monitor对象的Owner。那么这个线程持有的锁对象的Mark
Word就会变成重量级锁状态对应的结构,原本的hashcode等信息会被存入到Monitor对象中。等到线程释放锁的时候再被重置回来
2. 字节码指令中的monitorenter就是去关联monitor对象的流程。monitorexit就是线程释放锁并且重置MarkWord的流程.
如果synchornized中出现异常,也会出现monitorexit指令去释放锁
二、EntryList
EntryList:
可以把它理解成一个阻塞队列,因为同一时间只有一个线程能争抢到锁,那么没有争抢到锁的线程就会被阻塞(状态变成Blocked),也就是加入到Monitor对象的EntryList中,等待Owner中的线程释放锁后,再去重新争抢锁
三、WaitSet
当持有锁的线程调用了锁对象的wait方法时, 因为wait方法会去释放当前锁,所以当前线程就会从Owner退出来,加入到WaitSet中,并且状态变成Waiting状态,等待其他的线程重新唤醒
底层逻辑
- part 1
1.刚开始Monitor中Owner为null,Monitor中只能有一个Owner。
第一个线程访问同步代码块。首先会将synchronized中的锁对象对象头的MarkWord去尝试关联操作系统提供的Monitor对象,让锁对象中的MarkWord和Monitor对象相. 如果关联成功, 将对象头中的MarkWord的对象状态从01改为10,同时成为了该Monitor的Owner(所有者)
2.当第二个线程过来访问相同的临界代码的时候。首先会检查当前锁对象是否关联了monitor。
如果关联了monitor,则判断该monitor有没有owner,如果有owner则第二个线程关联当前monitor同时进入到当前owner的阻塞队列EntryList中。当第一个线程执行完临界代码或者中途异常释放锁之后,Monitor的Owner(所有者)就空出来了。此时就会通知Monitor中的EntryList阻塞队列中的线程,通过竞争, 成为同一个monitor的owner
- part 2
Monitor中有owner字段用于存储获得锁的线程信息,还有一个计数器count用于实现同一个线程锁的可重入性。
当线程拿到锁成为owner的同时,同时计数器count加1,如果同一个线程再次访问,计数器就会继续自增。
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒
若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
MarkWord详解
ptr_to_lock_record:指向栈中锁记录的指针
ptr_to_heavyweight_monitor:指向monitor对象(也称为管程或监视器锁)的起始地址
基础概念
- Java的对象头在对象的不同的状态下会有不同的表现形式,主要有三种状态,无锁状态,加锁状态,GC标记状态
- Synchronized上锁其实可以理解给对象上锁,也就是改变对象头的状态
- 借助JOL工具分析java对象中的MarkWord(demo省略)
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
- 一个java对象头包含2个word(如果是数组对象的话 还会有Array Length用来说明数组的长度)。 Mark word为第一个word根据文档可以知道它里面包含了锁的信息、hashcode、gc信息等等。klass word 为对象头的第二个word主要指向对象的元数据
- 2bit只能表示4种状态(00,01,10,11)JVM的做法将偏向锁和无锁的状态表示为同一个状态,然后根据图中偏向锁的标识再去标识是无锁还是偏向锁状态
- 虚拟机在启动的时候对于偏向锁有延迟,为了防止JVM内部某个线程调用你的线程。确保打印的对象头是偏向锁=》1 第一种是加锁之前先让线程睡几秒。2 加上JVM的运行参数 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 从而关闭jvm延迟偏向。
- 对象计算了HashCode,就不会进一步成为偏向锁了。
- 关键性理解点如下:
- 当调用wait后方法会直接变成重量锁。
- 轻量级锁和偏向锁之间的性能差距是巨大的,轻量锁和重量锁之间的性能其次。
- 偏向锁: 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
线程是不会主动释放偏向锁的 。锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。 如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头 - 当禁用偏向锁时,创建的对象为普通状态,即使该对象被synchronized修饰,也不会变为偏向锁状态
- 如果对象调用hashcode方法,会自动禁用偏向锁,是因为偏向锁的对象头中没办法存储hashcode。轻量级锁把Mark Word的值存放在栈帧中,重量级锁把Mark Word的值存放在Monitor中
下面谈谈锁的升级过程
-
偏向锁
偏向锁的作用
当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁
偏向锁的持有
锁第一次被拥有的时候,记录下偏向线程ID。同一个线程下一次访问的时候,不需要再次加锁和释放锁,而是直接比较对象头里面是否存储了指向当前线程的偏向锁
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
偏向锁的撤销
需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行
1. 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
2. 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 -
轻量级锁
轻量级锁的作用
在没有多线程竞争的前提下,通过CAS减少重量锁的使用,从而提升系统性能。适用于竞争较不激烈、代码块执行时间很短的情况。
轻量级锁的获取
当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头MarkWord中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁
轻量级锁的升级
自旋达到一定次数依然没有成功时,升级为重量级锁。JDK6以后,自旋次数是自适应的,根据同一个锁上一次自旋的时间和拥有锁线程的状态来决定轻量锁与偏向锁的区别和不同
轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁 -
重量级锁
重量级锁的作用
适用于有大量的线程参与锁的竞争,冲突性很高
重量锁的获取
轻量级锁升级之后就成为了重量锁
文章参考 :
https://blog.csdn.net/qq_36434742/article/details/106854061