synchronized的锁重入、锁消除、锁升级原理

1. 锁重入

synchronized(this) {
    synchronized(this){
        synchronized(this){
            synchronized(this){
                synchronized(this){         
                    ........                
                }            
            }        
        }    
    }
}

对应指令

monitorenter 
    monitorenter
        monitorenter
            monitorenter
                monitorenter
                ......
                monitorexit  
            monitorexit   
        monitorexit
    monitorexit
monitorexit              

在这里插入图片描述

2. 锁消除

不存在锁竞争的地方使用了synchronized,jvm会自动帮你优化掉,比如说下面的这段代码…

public void business() {
    // lock对象方法内部创建,线程私有的,根本不会引起竞争
    Object lock = new Object();
    synchronized(lock) {
         i++;
         j++;
         // 其它业务操作       
    }    
}

上面的这段代码,由于lock对象是线程私有的,多个线程不会共享;像这种情况多线程之间没有竞争,就没必要使用锁了,就有可能被JVM优化成以下的代码:

public void business() {
    i++;
    j++;
    // 其它业务操作
}

3. 锁升级

在这里插入图片描述

3.1 偏向锁

为什么要引入偏向锁

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

如何升级偏向锁

当锁对象第一次被线程A获取的时候,会将对象头中的标记位标记为01(表示偏向锁),并记录当前锁持有对象的threadID,因为偏向锁不会主动释放锁,因此以后线程A再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程A获取锁对象),则无需使用CAS来加锁、解锁;
在这里插入图片描述
注意:
线程A用完了锁之后,自己加锁时候修改过的Mark Word信息都不会再改回来了,也就是不会主动释放锁。
在这里插入图片描述

偏向锁的好处

当只有一个线程进入同步代码块的时候,提高性能。

3.2 偏向锁之重偏向

线程B去申请加锁,发现是线程A加了偏向锁;这时候回去判断一下线程A是否存活,
如果线程A挂了,就可以重新偏向了,重偏向也就是将自己的线程ID设置到Mark Word中。
如果线程A没挂,但是synchronized代码块执行完了,这个时候也可以重新偏向了,将偏向标识指向自己。
在这里插入图片描述

如果线程B在申请获取锁的时候,线程A这哥们还没执行完synchronized同步代码块怎么办?
升级轻量级锁

3.3 轻量级锁

轻量级锁的意义

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

偏向锁为什么要升级为轻量级锁

假如这个时候有线程A、B、C、D四个线程,线程A先加了偏向锁。之前讲过偏向锁只是在第一次获取锁的时候加锁,后面都是直接操作的不需要加锁。
这个时候其它几个线程B、C、D想要加锁,如果线程A执行时间较长,想重偏向都不行!这个时候就需要等线程A执行完4个synchronized代码块之后才能获取锁啊,哈哈,别的线程都只能看线程A一个人自己在那表演了,这样代码就变成串行执行了。

何时升级为轻量级锁

当锁对象第一次被线程t1获取并标记为偏向锁,这时候t2来竞争锁对象,由于偏向锁不会主动释放因此还是存储的t1的threadID,这时候查看t1是否存活,
如果t1没有存活,t2可以竞争将其设置为偏向锁(重偏向);
如果t1存活且继续不在持有这个锁对象,t2可以竞争将其设置为偏向锁(重偏向);
如果t1存活且继续持有这个锁对象,升级为轻量级锁;

升级轻量级锁

轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。
这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁。
在这里插入图片描述

偏向锁升级为轻量级锁的过程

  1. 首先线程A持有偏向锁,然后正在执行synchronized块中的代码
  2. 这个时候线程B来竞争锁,发现有人加了偏向锁并且正在执行synchronized块中的代码,为了避免上述说的线程A一直持有锁不释放的情况,需要对锁进行升级,升级为轻量级锁
  3. 先将线程A暂停,为线程A创建一个锁记录Lock Record,将Mark Word的数据复制到锁记录中;然后将锁记录放入线程A的虚拟机栈中
  4. 然后将Mark Word中的前30位指向线程A中锁记录的地址,将线程A唤醒,线程A就知道自己持有了轻量级锁
    在这里插入图片描述

轻量级锁模式下,多线程是怎么竞争锁和释放锁的

  1. 线程A和线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中
  2. 同时执行CAS操作,将Mark Word前30位设置为自己锁记录的地址,谁设置成功了,锁就获取到锁
    在这里插入图片描述

锁释放

将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。

轻量级锁模式下获取锁失败的线程应该会怎么样:自旋

获取锁失败之后的线程自己先原地等一段时间,然后再去重试获取锁,这种方式就叫做自旋。

monitor有一个_spinFreq参数表示最大自旋的次数,_spinClock参数表示自旋的间隔时间。所以自旋最多会重试_spinFreq次,每次失败之后等_spinClock的时间过后再去重试,如果尝试_spinFreq次之后都没有成功,那没辙了,只能沉睡了。

为了避免自旋时间太长,所以JVM就规定了默认最多自旋10次,10次还获取不到锁,那就直接将线程挂起了。

3.4 重量级锁

何时升级为重量级锁

如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的(默认10次),如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

总结

总的来说啊,JVM设计的这套synchronized锁升级的原则,主要是为了花费最小的代价能达到加锁的目的;

比如在没有竞争的情况下,进入synchronized的使用使用偏向锁就够了,这样只需要第一次执行CAS操作获取锁,获取了偏向锁之后,后面每次进入synchronized同步代码块就不需要再次加锁了。

然后在存在多个线程竞争锁的时候就不能使用偏向锁了,不能只偏心一个人,它优先获取锁,别人都看它表演,这样是不行的。
于是就升级为轻量级锁,在轻量级锁模式在每次加锁和释放是都需要执行CAS操作,对比偏向锁来说性能低一点的,但是总体还是比较轻量级的。

为了尽量提升线程获取锁的机会,没有获取锁的线程自旋等待;线程每次自旋一段时间之后再去重试获取锁。
由于自旋是空耗费CPU资源的,所以在尝试了最大的自旋次数之后;及时释放CPU资源,将线程挂起了。

原文:
synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值