网上的锁升级描述的都不是很全面,阿斌通过查阅众多的资料,以及阅读《深入理解java虚拟机》483页后,总结了这篇文章。
首先,synchronized的锁标志是存储在对象头的MarkWord中的。在64位的虚拟机中,他的MarkWord只有64位,能存储的空间也是有限的。于是,通过最后三位来作为模式标识,来存储不同模式下需要的不同信息,比如锁的升级情况。

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

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

匿名偏向锁
当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,只有最后一个有值,代表全部退出。

解锁:
注:不会主动进行解锁,出现竞争时才会解锁,这样做的目的是下一次同一个线程来获取锁时,直接检查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、轻量级锁状态发生并发时,会膨胀成重量级锁。
过程

主要结构:
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新增了一万的好友数,之前没加上好友的可以加一下我的个人微信,再晚又满了,一起抱团取暖,结伴内卷。

扫码拉群,学习打卡,交流经验
每周一读




