面试必问synchronized内存语义和锁升级锁优化、偏向锁、轻量级锁、重量级锁

1.锁基础概念

1.1 临界区 critical section

一段程序代码内如果存在对共享资源的多线程访问,称这段代码块为临界区,共享资源为临界资源

1.2 竞态条件 race condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。解决竞态条件的发生,可以有多挣手段可以解决:

        - 锁
        - 原子变量

1.3 公平/非公平锁

线程是否根据抢锁顺序执行这并不是公平、非公平的判断依据。公平锁和非公平锁的区别在于在入队之前是否尝试加锁。只要入队以后,就不存在公不公平的问题了。内置锁和AQS都是这个意思,只不过内置锁和AQS对抢锁顺序的策略有所不同罢了。

2.synchronized的底层原理

synchronized又称Java内置锁,底层是基于Monitor监视器机制实现,其内存原语是基于操作系统的Mutex互斥量进行的。JDK5之前内置锁是重量级的,性能较低,在JDK5之后JVM进行了对其优化,例如锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋等技术来避免了许多重量级开销,性能大幅提高。
synchronized在JVM层面是由两个指令MonitorenterMonitorexit实现。

2.1 管程/监视器 Monitor

管程是指管理共享变量以及对共享变量操作的过程,让其支持并发。管程的思想不仅仅局限于Java,操作系统都管程的思想。synchronized、wait()/notify()/notifyAll()都是管程技术的范畴。

2.2 MESA模型

管程的概念模型从发展上总共分为3大模型:Hasen模型、Hoare模型、MESA模型。目前使用实现最广泛的模型就是MESA模型。
在这里插入图片描述

2.3 synchronized管程和Monitor机制

JDK借鉴了MESA模型,对MESA进行了精简,Java内置锁只有1个条件变量来实现等待唤醒机制。java.lang.Object中的wait()/notify()/notifyAll()方法依赖于C++实现的ObjectMonitor对象实现,其ObjectMonitor的主要数据结构包括:对象头mark指针、锁的重入次数、锁对象、拥有监视器的线程ID、WaitSet等待队列、CXQ等待栈、EntryList等待队列。
在这里插入图片描述

在尝试获取锁的时候,将当前线程入栈CXQ,当释放锁时,如果EntryList为空,就将CXQ的线程出栈插入到EntryList中,并唤醒第一个线程;如果EntryList不为空,则直接从EntryList中唤醒线程。

2.4 锁记录和对象头

在这里插入图片描述

锁的状态被记录在对象头中MarkWord中:
在这里插入图片描述

2.4.1 MarkWord中锁标记枚举

在这里插入图片描述

2.4.2 锁状态

在这里插入图片描述

2.5 偏向锁

偏向锁是一种针对加锁操作的优化手段,在一般情况下线程并没有竞争,而是由同一个线程多次获得锁资源,为了消除无竞争产生的性能消耗,JDK引入了偏向锁,提高性能。

2.5.1 匿名偏向

JDK6开始默认开启了偏向锁模式,新new出来的对象MarkWord的ThreadID为0,说明该对象处于可以偏向,但未偏向任何线程的状态,称之为匿名偏向。

2.5.2 延迟偏向

HotSpot虚拟机在JVM启动后有4s的延迟才会对新new出来的对象开启偏向模式,这是因为JVM在启动过程中会有很多系统配置,这些类里面有很多内置锁,为了减少JVM启动时间,JVM提出了延迟偏向的功能。可以通过-XX:BiasedLockingStartupDelay=0来控制延迟时间。-XX:-UseBiasedLocking可以禁止偏向锁。-XX:+UseBiasedLocking开启偏向锁。

2.5.3 偏向撤销 - hashCode()

当调用对象的#hashCode()orSstem.identityHashCode()方法时,会导致偏向对象的偏向撤销。因为hashCode没有地方保存,所以撤销以后,这些hashCode等记录会根据不同的锁状态存在不同地方:轻量级锁存储在锁记录中。重量级锁存储在Monitor对象中。

当对象处于匿名偏向或已偏向状态下,调用对象的hashCode方法会导致对象再也无法偏向。

1.当对象处于匿名偏向时,调用hashCode()会让锁升级为轻量级锁。
2.当对象处于偏向锁时,在同步代码块中调用hashCode()会使偏向锁强制升级为重量级 锁。
偏向撤销是一个消耗性能的过程,一个好的程序流程不应该频繁的偏相关撤销,偏向撤销要等待全局安全点,会造成JVM的STW。

2.5.4 偏向撤销 - wait()/notify()

在偏向锁状态下执行notify(),会让锁升级为轻量级锁。执行wait()时,升级为重量级锁,因为wait()本来就是基于Monitor监视器实现。

2.5.5 锁重入

通过在栈中创建lock record记录来标识锁的重入次数,当同一个线程再次获取锁时,如果对象头中的线程ID是自己的话,无需CAS修改对象头。

2.6 轻量级锁

如果偏向锁失败,JVM并不会立即升级为重量级锁,而是通过轻量级锁来进行优化。轻量级锁的场景就是线程交替执行代码块。也就是说不存在锁的竞争,如果同一时刻多个线程抢锁,就会导致轻量级锁膨胀为重量级锁。
在这里插入图片描述

当偏向锁释放以后,锁状态仍然为偏向锁。此时,如果有另一个线程来加锁,则会升级为轻量级锁,会在当前线程中创建lcok record结构指向对象,存储锁的状态信息。通过CAS来修改对象头的指针信息。轻量级锁释放后会降级为无锁,将lock record中的信息拷贝回对象头。

2.7 重量级锁

如果在轻量级锁的模式下,发生了线程竞争,也即是说CAS修改对象头失败,那么当前竞争线程就会膨胀为重量级锁。重量级锁就会进入Monitor监视器模式。重量级锁的锁记录等信息保存在ObjectMonitor对象中,重量级锁释放后变为无锁。

2.8 锁升级/锁状态转移主流程

在这里插入图片描述

3.synchronized锁优化

3.1 批量重偏向

如果锁对象一直是同一个线程进行加锁,那么偏向锁的性能很高,但是当有竞争时,就会发生偏向撤销,转而升级为轻量级锁or重量级锁,这个开销蛮大。JDK为此进行了优化,方案就是批量重偏向和批量撤销。
批量重偏向的原理是:
以class为单位,每个class都会维护一个偏向锁撤销计数器,每当这个class的对象发生过一次偏向撤销,计数器就+1,当达到一个阈值(默认20次),JVM就认为该class的偏向锁有问题,转而进行批量重偏向。每次锁对象发生批量重偏向后,对象的epoch值就会+1,同时遍历JVM所有线程栈,找到这个class的所有被持有的锁对象,将其epoch值更新(只会更新正在锁定的对象)。线程下次获取锁时,发现当前对象的epoch和class维护的epoch值不相等,说明这个锁对象的偏向锁模式已失效,进而重偏向。
批量重偏向的机制是为了解决一个线程创建了大量对象进入偏向模式后,另外线程也将这些对象进行加锁操作,这些对象就会频繁偏向撤销,偏向撤销会消耗性能。偏向锁重偏向一次之后不可再次重偏向

3.2 批量偏向撤销

当class维护的偏向锁撤销计数器达到了阈值(默认40),JVM就认为这个class的所有锁对象的偏向模式有问题,将这个calss的所有锁对象置为不可偏向,后面有线程加锁,直接就是轻量级锁。新new出来的对象同样是不可偏向。但是这个计数器会有时间范围(默认25秒),过了这个时间就会重置清0。

3.3 自适应自旋

自旋发生在膨胀为重量级锁的过程中,因为最坏的情况,重量级锁是内核态,性能消耗大。在JDK6后,膨胀为重量级锁的过程中,尝试多次加锁,这个自旋是自适应次数的。

3.4 锁粗化

在一段没有线程竞争的程序中,例如方法体内局部变量,多次的加锁解锁,例如StringBuffer的#append(),JVM就会优化,进而扩大加锁范围,避免频繁加解锁。

3.5 锁消除 - JIT及时编译优化

4.Monitor、重量级锁原理

在这里插入图片描述

4.1 CXQ竞争队列

这是一个栈结构,线程在入栈之前会通过CAS自适应自旋操作来获取锁,实在获取不到才进入CXQ栈中(说明synchronized是非公平锁)。

4.2 EntryList等待队列

和CXQ一样时等待的队列,不过EntryList是队列结构FIFO,为了避免多线程并发修改CXQ问题,JVM引入了EntryList等待队列。当持有锁的线程释放后,JVM从EntryList中弹出一个就绪的线程作为竞争锁的线程Reday Thread,此时就绪线程并非owner,因为synchronized是非公平的,reday Thread不一定就能拿到锁。

4.3 WaitSet等待队列

持有锁的线程调用wait()方法时,就会放弃锁,进入WaitSet等待队列,等待其他线程调用锁对象的notify()、notifyAll()或超时等方法来唤醒,唤醒后会立即进入EntryList,走EntryList的流程。

4.4 park操作(Linux#Mutex)

线程的挂起操作是调用操作系统的API完成,这是一个系统调用,需要用户态到内核态的切换,Linux提供了pthread_mutex_lock函数来实现线程的park。用户态到内核态的切换有时比用户的同步代码执行时间还要长,所以synchronized才如此复杂繁琐的优化,尽可能避免park。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Minor王智

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值