synchronized 关键字的底层原理
jdk5 之前 synchronized 是重量级锁,但是jdk6 之后会有一个锁升级的过程
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、竞争较不激烈的情况、竞争激烈的情况三种情况。
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
适用于单线程适用的情况,在不存在锁竞争
的时候进入同步方法/代码块则使用偏向锁。 比如我们在使用StringBuffer中的很多带synchronized的方法,其实大多数时候都不会存在锁竞争的情况就使用的是偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
加锁流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
适用于竞争较不激烈的情况,存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁(CAS)
,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁
Synchronized 底层是由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁
,而在monitor内部有三个属性,分别是owner、entrylist、waitset,其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程; waitset 关联的是处于 waiting 状态的线程
① Monitor实现的锁属于重量级锁
,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
只有重量级锁才会关联Monitor,而对象是怎么关联上的Monitor的
重量级锁:适用于竞争激烈
的情况,如果同步方法/代码块执行时间很长(这种情况自旋的次数就会变多,增加系统的消耗)
,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
默认情况下,JVM中自旋的最大次数为10(可通过"-XX: PreBlockSpin
"参数进行修改),如果在这个次数后还未获得锁,则会升级为重量级锁。所以,如果我们希望减少轻量级锁向重量级锁的升级,可以适当增加自旋次数,例如将其设置为20-50。但是要注意,自旋次数越多,对CPU资源的消耗就越高。因此,需要在保证性能的前提下,根据具体的应用场景和硬件环境进行适当调整。
JVM 中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有把这个称为锁膨胀的过程),不允许降级。