JAVA锁系列(一)----syschronized锁的优化---看完再也不怕面试问到锁升级了!

Monitor对象:

锁机制的底层同步工具-----monitor

在每个java对象里,都存在一个内部锁----monitor锁
作为线程的私有数据结构:每个线程都有一个可用的monitor record列表
monitor中会有一个owner字段存放有一个唯一标识,表示该锁被我这个线程所占有。
对象里有对象头----对象头的底层结构由**markword(标记字段)和Klass pointer(类型指针)**构成,类型指针指向它的类元数据,来确定是哪个类的实例。
对象头的markword里有lockword指针----指向monitor的起始地址,monitor有个字段own字段再存放唯一标识。
monitor的本质是依赖于底层操作系统的Mutex Lock实现,线程之间切换需要将用户态切换到内核态,成本很高,jdk1.6已将syschonized优化为锁升级模式,不会直接变为重量级锁,会进行锁升级。

syschronized锁升级得完整过程:

无锁——>偏向锁——>轻量级锁——>重量级锁

一:无锁状态:

对象头前8字节markword得内存布局:

锁状态 26位 31位 3位 4位 1bit 2bit
无锁态 unused hashcode unamed 分代年龄 0 0 1

当资源数上升后,锁升级

二:偏向锁状态:

偏向锁: 对第一个线程进行偏向,它不需要申请,可以直接使用资源,jvm加锁的方法,stringbuffer等等在被调用时,实际上只有一个线程在运行,所以才会产生了偏向锁这种东西。

对象头前8字节markword得内存布局:

锁状态 54位 2位 1位 4位 1bit 2bit
偏向锁 当前线程指针 Epoch unused 分代年龄 1 0 1

一般再4s钟之后才会启动偏向锁。因为,当jvm刚运行的时候,里面有很多syn的方法,如果还是由偏向锁再到自旋锁,这样会消耗大量资源。所以在4s内jvm知道是多线程,所以直接跳过了偏向锁,转变为自旋锁

当资源继续变多后。锁继续升级。

三:轻量级锁—自旋锁状态:

对象头前8字节markword得内存布局:

锁状态 62位 2bit
自旋锁 指向线程栈中lockRecord的指针 0 0

当资源竞争变多时,我们将其升级为轻量级锁----CAS锁,多个资源进行争抢,都把值读取然后去修改并加上自己的id号,谁先改完往回放贴上id号,这个锁就给谁,剩下来只能在外面自旋,不断改值,再去比较,看看id号有没有被撤销,没又得话继续自旋。

PS:CAS简介:
CAS: compare and swap -----自旋锁

第一步:

读当前值E(从内存拿到线程cache里)——>计算结果V——>准备把值写回内存时——比较E值有没有发生改变,改变了说明其他线程修改过,CAS会继续读这个改过的值再次尝试计算结果,再写回主存,再判断有没有改过,。。。。产生自旋,也就是有可能死循环。

相同的话也有可能被其他线程改过,不过改的值刚好没变,这就是造成ABA问题。解决办法就是加一个版本号就行了,就专门的类

再自己的线程栈里生成lockrecord,当谁能将lockrecord得指针值写到这对象得对象头里,就算抢到锁了。lockrecord作用:记录上一个状态偏向锁得值。

但是自旋会消耗cpu资源,非常多得资源进行争抢时,只有一个资源能拿到,其他都在空自旋浪费资源,判断方式(1.6之前自旋得次数或者等待得线程超过cpu核数得二分之一,1.6之后就变成自适应,jvm自己优化)此时升级为重量级锁

四:重量级锁状态:

在这里插入图片描述
对象头前8字节markword得内存布局:

锁状态 62位 2bit
重量级锁 指向互斥量(重量级)的指针 1 0

重量级锁向内核态申请资源,当一个拿到锁后,剩下来得会进入一个等待队列,只有到你的时候,你才能运行,这样不会消耗cpu资源,但是会等的很慢。

引申概念:

锁粗化:

我们在使用同步锁时,需要将同步块作用范围尽可能缩小,使同步的操作数量缩小,让竞争等待锁的线程尽快拿到锁
但是会有一个情况例外,如果我们一直是对同一个对象进行不断的加锁解锁操作,本身这些操作就会造成不必要的性能损耗,jvm检测到后会将枷锁解锁操作移到for循环外,形成一个更大范围的加解锁操作,完成这个的过程叫锁粗化

锁消除:

如果一段代码的对象的数据,或者说共享的数据,其他线程压根不会去访问它,那就把它们当作栈上的数据对待,认为他们是线程私有得,可以将加锁去除,这其中的判断依据需要通过逃逸分析来分析完成。

猜你喜欢

转载自blog.csdn.net/qq_35050438/article/details/106368859