synchronized锁升级详细过程

copy: http://www.voycn.com/article/synchronizedxiangxijieshaozhisuoshengjiguocheng

 

前言

我们在并发编程过程中,会有一些资源或者操作是必须要进行序列化访问的,就是线程之间不能并发的访问,必须要进行串行访问,所以就引入了锁的概念,java中只用的锁主要有两种,一种是jdk内置的锁,一种是juc包下面的锁,jdk内置的锁是不要释放的,由jvm自动给我们释放锁,而juc包下面的锁是有Doug Lea大神开发的,在编程的时候需要在finally进行锁的释放,否则很容易导致死锁;

 

JVM线程调度原理

我们创建一个线程执行,最终会提交给内核态去执行,也就是由用户态切换到内核态,我们想一下,为什么需要线程池管理?

用户态创建的所有线程都最终都需要内核态去创建执行,那么如果不启用线程池,那么来一个线程就由用户态创建一个线程交给内核态去创建执行,那么这样的开销是非常大的,

就比如本文的主题是synchronized在jdk15之前就是采用内核态来控制的同步操作,在这种模式下,就是通过monitorEnter和monitorExit来控制的,而操作底层是通过mutex互斥信号量来实现的,那么这种情况下有锁的竞争就会在用户态和内核态之间反复切换,这样在性能就影响很大

那么在JDK1.6之后就引入了锁的优化,也就是锁升级,由无锁升级为偏向锁,如果竞争紧张,由升级轻量级锁,如果并发上来,升级为重量级锁,在这个过程中根据不同的业务场景选取不同的锁来实现

举个例子,两个人同时要进一个房间,这个房间是有钥匙的,钥匙在前台管理员处,比如第一个人首先拿到管理员的钥匙,进入了房间,那么第二人过来也要进这个房间,如果他会去管理员处不断询问是否可以进,如果可以,管理员就拿钥匙给他,但是如果第一个人迟迟没有出来,那么第二个人会一直不停的去询问,直到第一个人出来为止,那么如果门上如果有个标记,表示房间有人,直到进入房间的人出来过后标记清除,那么第二个人就可以进入了,那么这样是不是效率要更高一点,避免了反复去管理员处询问房间是否可以使用的的性能开销。

所以这里把管理员比喻成操作系统内核而这两个人比喻成两个线程,而房间是一个对象,那么对象上不打标记,那么第二个线程会反复的去操作系统内核判断是否可以进入同步代码块,那么就会有反复的进行用户态与内核态直接的切换,这样的性能开销是非常大的,而采用对象打标机的模式,那么就只有在用户态之间切换,这样就提升了性能,避免了不必要的性能开销,这也就是synchronized在jdk1.6过后的锁优化达到的效果,也就是通过锁升级来实现的

 

JVM线程调用过程

 

 

JAVA中创建线程由用户态切换到内核态进行创建线程,也就是上面所说线程是操作系统调度的最小单位

 

JAVA线程与内核线程的关系

 

 

Synchronized锁

加锁方式

 

 

总结:
同步方法锁:锁的是当前实例对象
同步静态方法:锁的类对象
同步代码块:锁的是指定对象

原理

互斥性:synchronized修饰的方法、代码块只能由一个线程进行访问,其他线程只能阻塞等着;
可见性:某线程 A 对于进入 同步块之前或在 synchronized 中对于共享变量的操作,对于后续的持有同一个监视器锁的其他线程可见。
在早期的synchronized中的同步锁实现比较简单,我们通过实例类分析,看下面的代码:

 

 

 

 

 lock状态(2bit):
1.01是无锁或者偏向锁;
2.00是轻量级锁;
3.10是重量级锁;
4.11是GC标记,表示可以被GC了,被GC打了标记了。
biased_lock(1bit):是否偏向锁的标志,0=否 ,1=是
age(4bit):对象的分代年龄,占4bit,所以分代年龄的最大年龄是15,设置智能是小于等于15;
unused:表示未使用的
epoch(2bit):表示偏向锁的时间戳
identity_hascode(31bit):对象的hashcode值
ptr_to_lock_record(62bit):轻量级锁状态下,指向对象监视器的monitor的指针
prt_to_heavyweight_monitor:重量级锁状态下,指向对象监视器的monitor指针

 

 

 

 

无锁升级为偏向锁

在JDK1.6过后,默认是开启了偏向锁的,偏向锁的性能较低,偏向锁适用于单线程的环境下,所以要根据具体的业务情况来使用,如果synchronized在第一个线程进入的情况下,默认修改为偏向锁,将当前线程的ID更新到对象头的markword的线程ID中,增加偏向锁的时间戳以及偏向锁的标志修改为1

 

 比如有几个线程同时访问同步代码块,那么只有一个线程可以进入,那么初始的object markword肯定是无锁的,也就是上图的无锁对象头,那么这个时候线程1把当前线程的ID通过CAS修改到markword中,这个过程中的CAS肯定是能成功的,不成功就不是无锁升级为偏向锁了,还是其他锁升级的过程了;这个时候其他线程是在线程1未退出同步代码块的时候是没有八法进入同步代码块,也就是没有办法获取锁,那么其他获取cpu执行权限的线程会通过CAS修改线程ID为当前线程,但是如果线程1没有退出同步代码块,而后续线程通过CAS进行修改是不能成功的,那么这个时候后续的线程就将synchronized升级为轻量级锁,也就是下一个锁升级过程

偏向锁升级为轻量级锁

偏向锁升级为轻量级锁是在线程有竞争的情况下,线程1迟迟没有退出同步代码块,而线程2又要竞争这把锁,而线程2通过CAS自适应自旋一直没有成功,这个时候它就升级为轻量级锁,如果在CAS的过程中,线程1退出了同步代码块,那么这个时候线程2CAS成功,是不会升级为轻量级锁,所以偏向锁适用于单线程的环境下

 

 轻量级锁是在线程有一定的竞争的时候想要进入同步代码块,而如果这个时候之前运行在synchronized的线程退出了,那么不会升级为轻量级锁,还是偏向锁,如果线程2cas结束过后,线程1还没有退出就会进行锁升级,偏向锁升级为轻量级锁,这个升级过程非常消耗性能,所以有很多公司都是禁止出现偏向锁的,因为偏向锁升级为轻量级锁的时候,是需要撤销偏向锁的,撤销偏向锁的过程如下:
1.在一个安全点停止所有拥有锁的线程;
2.遍历线程栈,如果存在锁记录,需要修复锁记录和MarkWord,使其变成无锁的状态;
3.唤醒当前线程,将当前锁升级为轻量级锁;
所以这个过程是非常消耗性能的,所以不适合在多线程的环境下使用偏向锁

轻量级锁升级为重量级锁

 

 

重量级锁在并发非常高的情况下启用,就是锁的竞争非常激励,比如线程1首先将在线程栈上开辟一定的空间来存储mark word,并且相互指向,然后开始执行同步代码,而这个时候很多线程都过来了,那么这些线程也要拷贝markword到线程栈中,然后cas修改lock record与mark word的相互指向,这个时候只有一个线程能够成功,其他线程都需要cas,如果线程1没有同步代码块没有指向完成,其他线程是没有办法自旋成功,那么就就那些锁膨胀,升级为重量级锁,重量级锁升级过后线程的阻塞是由内核进行处理的,所以性能较低。

GC标志

我们知道GC在每次进行的时候其实就是对对象的操作,对象的对象头中的markword进行操作,如果这个对象可以被GC了,那么GC会在在markword的锁状态设置为11,表示新一轮的gc开始了,而对象的age是最大15次,每次gc,age+1,如果达到了15次还存活就移植到老年代;所以这里有个问题就是如果我们的object对象升级为重量级锁了,那么是不是一直是重量级锁呢?我们知道锁的升级是不能降级的,也就是说轻量级锁不能降级为偏向锁,偏向锁不能降级为无锁,那如果说我们的锁升级为重量级锁了,过了很久都没有线程来访问,下一次线程来访问的时候还是重量级锁吗?不是的,JVM没有这么的傻,也就是说在很久没有线程访问的情况下会进行降级,但是降级是直接降级为无锁状态。

降级的目的和过程

因为基本对象锁的实现优先于重量级锁的使用,JVM会尝试在SWT的停顿中堆处于空闲状态重量级锁进行降级操作,这个降级过程是如何实现的呢?我们知道在STW时,所有的JAVA线程都将暂停在安全点SafePoint,此时VMThread通过对所有Monitor的遍历,或者通过对所有依赖于MonitorInUseLists值得当前正在使用中的Monitor子序列进行遍历从而得到哪些是未被使用的Monitor作为降级对象。
可以降级的Monitor对象
重量级锁的降级过程发生在STW阶段,降级对象就是哪些仅仅能被VMThread访问而没有被其他JavaThread访问的Monitor对象。

锁的优缺点

在这里插入图片描述

 

posted @ 2020-11-17 21:03  随心的风  阅读(2268)  评论(0编辑  收藏  举报