暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

谁说synchronized锁只能升级不能降,八股文白背了?

阿斌Java之路 2022-07-26
1374

网上的锁升级描述的都不是很全面,阿斌通过查阅众多的资料,以及阅读《深入理解java虚拟机》483页后,总结了这篇文章。

首先,synchronized的锁标志是存储在对象头的MarkWord中的。在64位的虚拟机中,他的MarkWord只有64位,能存储的空间也是有限的。于是,通过最后三位来作为模式标识,来存储不同模式下需要的不同信息,比如锁的升级情况。

img

首先,一个对象是否开启偏向锁是根据某个参数来决定的

  • 开启偏向锁


    • jvm启动4秒后进入匿名偏向锁状态:默认有偏向延迟,避免jvm线程一开始去抢占锁,导致产生了偏向锁。启用安全点会带来STW。而偏向锁的撤销与重偏向判断,也是需要启用安全点的。
    • 关闭偏向启动延迟,对象一创建就进入匿名偏向状态。
  • 不开启偏向锁


    • 对象一创建就会进入无锁状态。
img

再来看看锁整体的升级流程

img

匿名偏向锁

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁

加锁

1、判断当前偏向锁的状态是开启的也就是101, 通过CAS操作将当前线程的地址设置到锁对象的markword中。如果设置成功了,那么就是设置偏向锁成功了。(CAS 中 原ID为0,目标ID为线程ID)

2、在当前线程的栈贞中,创建锁记录(Lock Record),使这个锁记录锁标识指向锁对象。这个是不需要CAS操作的。

3、-XX:-UseBiasedLocking
 可以关闭偏向锁,默认是开启状态的。

注:期望原锁是匿名偏向锁也就是原线程id为0,否则就进入下述的安全点检查,偏向锁竞争

偏向锁多次的重入,会在栈帧顶部生成多次的lock record。每退出一次,就移除一个record,只有最后一个有值,代表全部退出。

img

解锁:

注:不会主动进行解锁,出现竞争时才会解锁,这样做的目的是下一次同一个线程来获取锁时,直接检查mark word的锁记录就可以了。

过程:在B线程获取偏向锁时,查看mark word的线程id不是自己的,那么B线程就会向VM的线程队列发送一个撤销偏向锁的任务,VM线程会不断检测是否有任务要执行,当检测到这个任务后,就需要在安全点去执行(安全点时,JVM内的所有线程都会被阻塞,只有JVM线程处于运行状态,它可以执行一些特殊的任务,如full gc就是此时执行)

注:因为撤销锁的时候,会修改持有锁线程的栈数据,如果不在安全点执行的话,会有并发问题。

1、需要等待全局安全点(在这个时间点上没有正在运行的字节码)。它会首先暂停拥有偏向锁的线程,然后检测这个线程是否存活

2、如果不存活的话,那么就先将对象头设置为无锁状态,并偏向提交撤销锁的那个线程。

3、如果是存活状态,

存在竞争:那么就先将对象头设置为无锁状态,并升级为轻量级锁

这里的竞争是通过查看锁记录中,是否还有指向锁对象的记录,存在表示还在同步代码块中,即上一个持有锁的线程还没有执行完。

没有竞争:那么就先将对象头设置为无锁状态,并偏向另一个线程。

4、当偏向锁的撤销次数超过40次后,会直接升级为轻量级锁。

注:偏向锁升级的过程

1、遍历栈贞中锁记录,找到第一条记录,修改这条记录的displaced数据为锁对象的无锁状态mark word值。也就是偏向锁持有的栈帧中需要把占位的lock record改成无锁状态下的mark word。

2、修改锁对象mark word的状态为轻量级锁状态,并且保留其中的线程内存地址。

注:偏向锁的优势是只有一个线程的情况下,重新获取锁甚至不需要cas,但是偏向锁的竞争消耗非常大,甚至超过轻量级锁。所以偏向锁升级后,再也回不到偏向锁了。因为轻量级锁执行完后,会恢复成无锁。

轻量级锁

加锁

1、在当前栈帧中创建一条锁记录,锁记录内的锁引用字段保存锁对象的地址

2、锁对象生成一条无锁状态的markword值,学名叫displacedMarkWord,然后将这个值保存到锁记录的displaced字段内

3、使用CAS去设置当前markword的值,修改为当前线程持有轻量级锁状态。

更新为轻量级状态成功,那么就是获取轻量级锁成功

如果失败,那么就会检测为什么失败,第一个要检测的就是是否是锁重入,检测到锁对象的markWord中锁持有者指向当前的线程,那么就是锁重入。这时只需要把在栈中插入的锁记录的displaced字段置为空就可以了。(即只有第一次加轻量级锁时,锁记录的displaced才有值)

每次锁重入,都会记录锁记录,可以用来统计重入次数。

注:简单来说,就是线程开辟一个rocord来保存markword无锁状态的值,然后markword存轻量级锁模式下的本线程id

解锁

1、检测最后一条锁记录,会先将栈帧中锁记录的锁对象引用设置为null

2、检测这条锁记录的displaced字段是否有值,没有值证明这时一次锁重入,那么清除掉这条记录就可以了,步骤就是上面的将锁记录的锁对象的引用设置为null

3、每一次重入锁的释放都是这样的操作

4、然后就是最后一条锁记录的释放,这条锁记录是有displaced值的,这一步的释放是通过CAS将锁记录中的displaced值复制到锁对象中。

5、如果成功就是释放了,如果失败就有可能是锁已经膨胀了或者正在膨胀。

注:最后个lockrecord中的displaced是有值的,代表我们需要将原来保存的无锁mark word设置回去。将对象恢复成无锁状态。如果失败了,说明之前有线程竞争膨胀成重量级锁了。我们还需要在释放锁的同时,唤醒被挂起的线程。

重量级锁

加锁

1、有线程调用轻量级锁或偏向锁对象的hashcode方法,因为这个时候mark word是没有办法存储hash值的,所以需要膨胀到重量级锁

2、持锁的线程调用锁对象的wait()方法

因为锁对象处于偏向或者轻量级锁的状态下,是没有管程对象和等待队列的,所以无法保存线程节点

3、轻量级锁状态发生并发时,会膨胀成重量级锁。

过程

img

主要结构:

ContentionList:等待队列

EntryList:存放等待锁而被block的线程队列

WaitSet:线程调用await()方法后,则线程会加入到waitSet中

Owner:表示当前持有锁的线程

(1)膨胀到重量级锁之后,当一个线程想要获取锁的时候,先去锁对象的mark word中获取管程对象的地址,进入管程后先尝试几次获取锁,就是通过CAS将管程内的owner字段设置为当前线程。设置成功就是获取锁成功了,没成功的话就是有占用。

(2)有占用时先封装成一个ObjectWaiter对象,存入到EntryList的队首,然后调用park挂起当前线程。底层就是通过Linux的mutex互斥量来实现

(3)当线程释放锁的时候,会从等待队列或者EntryList中选择一个线程进行唤醒,被选中的线程叫假定继承人,因为在它获取锁的过程中,外部线程有机会获取到锁,这也是Synchronized是不公平锁的原因。

(4)当持锁的线程调用await()方法时,会将线程加入到waitSet中,当被Object.notify唤醒后,会从waitSet移动到EntryList,继续等待获取锁。

解锁

1、设置owner为null,这个时候其他线程可以获取锁,所以是不公平的。

2、如果没有等待的线程则直接返回就可以了

3、如果有的话,就根据不同的策略去唤醒线程,唤醒的线程重新尝试去获取锁,即入等待队列和EntryList..

注:这个唤醒策略很多,默认的情况下是,从EntryList中获取,如果EntryList没有线程,那么就从等待队列中按原有顺序插入到EntryList中,这就意味着先插入到等待队列的线程,在EntryList中是最后被唤醒的

注:像HotSpot JVM 其实就支持锁降级,但是锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。

JVM会尝试在SWT的停顿中对处于“空闲(idle)”状态的重量级锁进行降级(deflate)。这个降级过程是如何实现的呢?我们知道在STW时,所有的Java线程都会暂停在“安全点(SafePoint)”,此时VMThread通过对所有Monitor的遍历,或者通过对所有依赖于MonitorInUseLists值的当前正在“使用”中的Monitor子序列进行遍历,从而得到哪些未被使用的“Monitor”作为降级对象。

降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。

被锁的对象都被垃圾回收了有没有锁还有啥关系?因此基本认为锁不可降级。

参考

https://blog.csdn.net/chengyan_1992/article/details/124803701





END



后台回复关键词 打卡 获取今日推荐资料

微信8.0新增了一万的好友数,之前没加上好友的可以加一下我的个人微信,再晚又满了,一起抱团取暖结伴内卷。



扫码拉群,学习打卡,交流经验


每周一读




文章转载自阿斌Java之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论