谈谈Synchronize锁升级的过程

前言

Synchronize作为同步机制中的一大杀器,常常被用于多线程中解决并发问题,在面试中也常作为悲观锁的考察对象【关于乐观锁和悲观锁,感兴趣的话,可以参考我的另一篇博文谈谈个人对乐观锁、悲观锁的理解】。本文不对Synchronize的底层原理进行分析,而是针对面试中常考的一个问题——谈谈Synchronize锁升级的过程表达一下自己的看法,首先介绍一下锁升级涉及到哪些锁。


正文

对象头Header的结构
在这里插入图片描述

1、偏向锁

偏向锁的JDK1.6引入的一项锁优化,为什么要引入它呢?因为经过HotSpot(JDK使用的虚拟机)的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。


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

当一个线程A进入被修饰的代码块并获取锁对象时,会在对象头中和栈帧中写入当前持有这个偏向锁的ThreadID,偏向锁不会主动释放,当线程A想要再次获取锁时,会比较Java对象头中的ThreadID和当前想要获取这个锁的ThreadID相比较,如果相同,就不用通过CAS来解锁、加锁,而是直接拥有;如果不一致,那么就需要通过Java头中的ThreadID来查看这个Thread是否存活,如果不存活,那么直接将锁对象置为无锁状态,然后当前线程就能拥有偏向锁了;如果存活,那么就需要查看栈帧,判断线程A是否还需要锁,如果不需要,则将锁对象置为无锁状态;如果需要,就将线程A暂停,然后撤销偏向锁,将其设为无锁状态或升级为轻量级锁(标志位置为00)。

2、轻量级锁

轻量级锁是JDK1.6引入的一项锁优化,在介绍轻量级锁之前,先介绍一下自旋锁。


自旋锁

自旋锁在JDK1.4.2中引入。虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

轻量级锁

轻量级锁考虑的场景是锁的竞争不大,而且锁的持有时间较短,因为阻塞线程需要将CPU从用户态转到内核态,这需要较大的开销,如果线程刚进入阻塞状态,又因为锁的释放而被唤醒,那么CPU花费多大代价过大,所以我们考虑不阻塞线程,而让等待的线程自己“忙”一段时间(自旋)。这就是轻量级锁。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机将首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后将对象头中的 Mark Word 复制到锁记录中。

拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。


轻量级锁升级为重量级锁的过程

线程自旋需要消耗CPU,因此自旋不能一直进行下去,当自旋达到一定次数时,线程A还没有释放锁,或者线程2在等待过程中,又有第三个线程3来等待获取锁,那么此时轻量级锁就会膨胀为重量级锁,重量级锁会把所有除了拥有锁的线程都阻塞掉,以防止CPU空转。

3、重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁的获取过程是通过monitor来实现的。以Synchronize为例。

Synchronized

Synchronized是最常用的线程同步手段之一,90%的线程同步问题都可以使用Synchronized来解决,那么它是如何保证同一时间段内只有一个线程能进入临界区呢?我们通过Synchronized分别修饰代码块和方法进行介绍。首先介绍一下什么是Monitor对象。

  • Monitor对象:在JVM中,对象分为3个部分:Header、Instance Data和Padding。

    • Header: Header包含2部分数据,Mark word(标记字段)、Klass Point(类型指针)

    • Mark Word:默认保存了对象的HashCode、分代年龄和锁标志位信息,可以看到,Mark Word的值会根据锁标志位的变化而变化。

    • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。
      在这里插入图片描述

当Monitor被某个线程持有后,Owner就会指向这个持有它的线程,_EntryList 用来保存已经获取到锁的线程,_WaitSet用来保存等待获取锁的线程。

修饰方法:使用Synchronized修饰方法时,在方法的字节码上会加上一个标志位ACC_SYNCHRONIZED,当其他线程进入这个方法时,会查看方法是否有这个标志位,如果有,就说明这个锁已经被其他线程占用了,当前线程就不能执行这个方法。

修饰代码块:Synchronized修饰代码块时,是通过monitorenter和monitorexit来实现的,,每个对象都对应着该monitor,当一个monitor被拥有之后就被锁住,其他线程就不能运行到monitorentr指令时,会由于无法获取monitor而陷入阻塞,monitor内部维护这一个计数器,这个计数器记录了当前monitor被拥有的次数,当前拥有它的线程可以重复拥有它,当计数器为0时,表示可以释放当前锁了,于是就执行monitorexit指令,此时其它线程就可以获取锁了。

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值