synchronized锁升级原理剖析 ✨ 每日积累
synchronized锁升级原理剖析在jdk1.6之后,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括如偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock、Coarsening)等,这些技术都是为了在线程之间
synchronized锁升级原理剖析
在jdk1.6之后,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括如偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock、Coarsening)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
锁交互流程图
各种锁状态在64位虚拟机中存储的结构
锁升级流程
无锁 —→ 偏向锁 —→ 轻量级锁 —→ 重量级锁
当创建出来的对象后一开始是属于无锁状态,如果对象可以偏向的对象且需要加锁会初始锁定变为偏向锁,如果无法达到使用偏向锁的条件(既锁竞争现象),转换为轻量级锁,如果轻量级锁依旧不满足使用则先进入适应性自旋进行锁的抢占,在一定自旋中还没抢占到锁,升级为重量级锁。
偏向锁
锁不仅不存在多线程竞争而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。减少不必要的CAS操作。 这个锁偏向于第一个获取到它的线程,会把对象头中的 Mark Word 的更新通过cas操作指向Lock Record(锁记录)的指针,Lock Record中的owner属性指向当前对象,表示锁的对象。如下图所示
一旦出现多个线程竞争时,撤销偏向锁,锁升级。
偏向锁原理
检测Mark Word是否为 可偏向状态,即是否为偏向锁1,锁标识为为01。
若为 可偏向状态,则测试线程ID是否为当前线程ID,如果是,执行同步代码块,否则执行步骤(3)
如果测试线程ID不为当前线程ID,则通过CAS操作将Mark Word的线程ID替换为当前线程,执行同步代码块
偏向锁撤销过程
偏向锁的撤销动作必须等待全局安全点(当前线程跑到最近的安全点【safe point】,然后停顿下来)
暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态;
偏向锁好处
持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都不再进行任何同步操作,减少资源消耗
轻量级锁
轻量级锁是JDK1.6当中为了优化synchronized而引入的一种新型锁机制;需要注意的是轻量级锁不是任何情况下其开销比较的小而是在特定的情况下才开销比较小;所以轻量级锁并不能够用来代替重量级锁;轻量级锁只是在一定的情况下来进行减少消耗;
轻量级锁原理
判断是否为无所状态(锁标志位为01)
是则把对象头中的 Mark Word 的更新通过cas操作指向Lock Record(锁记录)的指针,Lock Record中的owner属性指向当前对象。
将锁标志位设置为00
轻量级锁的释放
取出在获取轻量级锁 保存在Displaced Mark Word中的数据;
用CAS操作 将取出的数据 替换当前对象的Mark Word中,如果成功,则说明释放锁成功;
如果CAS操作失败,锁膨胀,升级为重量级锁
轻量级锁好处
在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
锁膨胀
当轻量级锁发生竞争的时候会膨胀升级为重量级锁; 对象头中的Mark Word指向重量级锁Monitor对象,设置锁标记位为10
锁自旋
当有多线程竞争锁的情况下,并不期望直接升级为重量级锁,重量级锁带来的性能开销是很大的,当升级为重量级锁之前,会有一个适应性自旋操作,在一定的自旋次数下一直尝试获取锁,避免直接升级为重量级锁,这个行为就是锁的自旋。 自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改;
适应性自旋锁
在JDK 6中引入了 自适应的自旋锁。自适应 意味着 自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机 就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如20~100次循环。
锁消除
在JDK6当中除了这个锁升级的优化之后还会有一个锁消除的优化; 锁消除的主要判定依据 来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而不被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无需进行。 去除不可能存在共享资源竞争的锁,通过这种方式消除
public class TestLockElimination {
private static void test(String str1, String str2){
/*这种情况下发生多线程调用这个方法时,每一个线程都会new一个新的
StringBuffer对象,append的方法时锁的this对象,
当前代码不会涉及到锁竞争,所以同步代码块或者方法会被消除*/
new StringBuffer().append(str1).append(str2);
}
public static void main(String[] args) {
test("1", "2");
}
}
//-------------------StringBuffer类中的append方法------------------------//
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
锁粗化
为了多线程能高效并发,我们会把同步代码块的范围设置的尽可能小,但是某种情况下,会导致一个线程重复经入代码块,多次上锁、锁释放造成增加性能消耗, 如果遇到这种情况都是用一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。
public class TestLockCoarsening {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
/*append方法为同步方法,
同一线程多次进入同步方法会导致性能开销,
jvm会将synchronized加到for循环外面*/
stringBuffer.append(i);
}
}
}
//-------------------StringBuffer类中的append方法------------------------//
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
更多推荐
所有评论(0)