谈谈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。

扫描二维码关注公众号,回复: 11383721 查看本文章

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 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指令,此时其它线程就可以获取锁了。

猜你喜欢

转载自blog.csdn.net/qq_37163925/article/details/106147535