偏向锁和重量级锁的多连问,你能接住几个?

前言

对于Hotpot JVM中的偏向锁,大部分开发者都比较熟悉或者至少听说过。那我们用下面10个关于偏向锁的进阶问题,检验一下自己离精通还有多远。

  • 如何判断当前锁对象为偏向锁
  • 偏向锁如何判断锁重入
  • 当代码运行至synchronized修饰的代码块时,符合什么条件才会尝试获取偏向锁
  • 线程进入偏向锁后,会不会创建lock record
  • 偏向锁膨胀后,lock record有什么变化
  • 如何判断当前持有锁的线程已经因为批量重偏向,而被撤销了偏向锁
  • 批量撤销和批量重偏向的触发条件是什么
  • 批量重偏向后,lock record和锁对象有什么变化
  • 批量撤销后,lock record和锁对象有什么变化
  • 批量撤销/重偏向后,新创建的锁对象,是否支持偏向锁

看了上面的问题,如果是胸有成竹,那就可以跳过这篇文章了。如果一脸问号,这篇文章应该对你有所帮助。

名词解释

首先明确下文章中用到的名词,因为不同人可能叫法不一样。
对象头,Java对象在堆中存储时,会按照对象头加实例数据的结构来存储。这篇文章只讲锁,所以一般是指对象头中的Markword部分。
klass对象,jvm在加载类之后,会在堆内存中生成该类的对象,就是我们代码中this.getClass()获取的对象。
锁对象, synchronized指定的锁对象。对于普通方法,这个对象默认是this指针。对于静态方法,锁对象是堆里的class对象。
Lock record,进入synchronized时在线程栈中生成的锁记录,对这个不熟悉的可以百度一下或看一下《深入java虚拟机》这本书
锁膨胀,hotspot中从轻量级锁升级成重量级锁称之为膨胀,为了便于理解,通常把偏向锁升级成轻量级锁也称为膨胀。

问题解析

问题1:如何判断当前锁对象为偏向锁
这个问题比较简单,一般了解过对象头或者偏向锁的都比较熟悉。当锁对象为偏向锁时,Markword的偏向锁标识位为1,锁标识位为01。即markword的最后3位为101。

问题2:偏向锁如何判断锁重入
接上面问题的Markword结构,当已经有线程获取到偏向锁,它的id就会填到markword中的线程id中。重入时线程只要检查thread id里存的是否就是自己线程的id就可以了。

问题3:符合什么条件才会尝试获取偏向锁
首先,hotspot中通过参数UseBiasedLocking控制是否启用偏向锁,不设置时默认是启用的。如果想要禁用偏向锁,可以在启动参数中添加-XX:-UseBiasedLocking。

是不是这样回答这个问题就结束了呢?答案是否定的。hotspot还有一个延迟偏向的概念,就是在jvm启动的时候是有一个延迟时间,过了这段时间后偏向锁才开始启用。这个延迟时间通过启动参数BiasedLockingStartupDelay来设置,默认为4秒。那延迟的目的是什么呢?hotspot的解释是在jvm启动过程中,内部有多个逻辑会用到锁,比如类加载。如果一开始就启用偏向锁,就导致频繁的撤销偏向锁,偏向锁的撤销需要在安全点执行,这样有可能影响jvm启动的速度。

满足上面2个条件之后,是不是就愉快的进入偏向锁了呢,其实还要经过2关。

第三个条件就是锁对象没有膨胀,如果锁对象已经膨胀成轻量级锁了,那就不会再走偏向锁了。这就是经常说的锁只支持升级,不支持降级。轻量级锁的markword如下:

最后,如果锁对象对应的class发生了批量撤销的动作,也不会再进入偏向锁了。比如有10个锁对象lockobj0..lockobj9,他们都是LockObj类的实例,如果发生偏向锁的批量撤销,那在这10个锁对象上的抢锁操作都不会再走偏向锁逻辑。

问题4:线程进入偏向锁后,会不会创建lock record
了解轻量级锁逻辑的都知道,轻量级锁加锁后,锁对象会保存lock record的引用,关系如下:

那偏向锁有没有呢?答案是有的。其实轻量级锁的这个lock record在运行至synchronized的时候就创建了,这个时候jvm还不知道具体使用的是偏向锁还是轻量级锁,偏向锁和轻量级锁用的是同一个lock record。偏向锁的时候,对象头里没有lock record的指针。

但是,我们再深挖一层,是不是每次都会创建?答案是否定的。比如在同一个方法中,对同一个锁对象的重入,就不会再次创建lock record,比如下面的代码(虽然不会有人这么写代码😄):

    public void testSync() {
        synchronized (this) {
            //first time
            synchronized (this) {
                // second time
            }
        }
    }

问题5:偏向锁膨胀后,lock record有什么变化
首先,来看下膨胀前的lock record和锁对象,它们的关系如下:

偏向锁lock record


栈中的lock record包含了指向锁对象的指针和markword的副本。
锁膨胀后可能出现两种情况:
1)抢锁线程获得了轻量级锁,则替换lock record中的displace_header的锁状态位为无锁。
2)如果是轻量级锁的锁重入,则会降lock record的displace_header设置为空
3)其它线程持有轻量级锁,则会膨胀成重量级锁,这时候lock record已经没用了,会将将markword锁标记为设置为011,代表已经不使用了
 

问题6:如何判断持有锁的线程已经因批量重偏向被撤销

当发生批量重偏向时,jvm会将klass对象的markword.epoch+1。并且遍历所有该类型的锁对象,如果加锁的线程仍然存活,则也会将锁对象的epoch设置成跟klass一样。

所以,如果另外一个线程在进入偏向锁逻辑时,发下锁对象的epoch跟klass的epoch不相等,则可以肯定该偏向锁已经被撤销。
 

问题7:批量撤销和批量重偏向的触发条件是什么

jvm通过两个参数来控制何时触发批量重偏向和批量撤销。

  • BiasedLockingBulkRebiasThreshold,批量偏向阈值,默认值20。
  • BiasedLockingBulkRevokeThreshold,批量撤销阈值,默认值40。

当同一类型的锁对象上发生锁争抢累计达到这两个数字时就会触发批量重定向和批量撤销。

划重点,这两个累计值是在klass对象上,不是锁对象上。

问题8:批量重偏向后,lock record和锁对象有什么变化
可以参考问题6,批量重偏向后,klass对象和仍然活着的线程持有的锁对象,epoch会加1。也就是说,当前线程抢的偏向锁的持有线程如果挂了,那epoch不会变,就会被抢锁线程撤销或重偏向到当前线程。
 

问题9:批量撤销后,lock record和锁对象有什么变化
批量撤销后,klass和所有相同锁对象的偏向锁都会被撤销,markword的锁标识位变成无锁。
 

问题10:批量撤销/重偏向后,新创建的锁对象,是否支持偏向锁

  • 批量重偏向后,新创建的锁对象,默认仍然是偏向锁。
  • 批量撤销后,新创建的锁对象,默认都会是轻量级锁(无锁)。因为发生批量撤销后,klass对象的markword锁标识位变成无锁,所以在这之后创建的锁对象,默认跟klass对象的markword相同。

总结

jvm因为加入了偏向锁逻辑而大大提高了同步锁的速度。但是偏向锁不是万能的,尤其是现在互联网应用并发越来越高,偏向锁在过多的争抢下反而会影响效率并且很快就会发生膨胀,已经越来越偏离了了它设计时的初衷。当前的Java应用中也基本会使用JUC包来做并发的同步,偏向锁的使用场景越来越少。当然硬件性能的提升也在削弱偏向锁的优势,所以Java15默认关闭了偏向锁。当然,本篇文章对于你参加面试还是能够提供一点点帮助的😄。

重量级锁的8连问

接上面偏向锁的十连问,继续升级到重量级锁的进阶版,检验一下自己离精通重量级锁还有多远。建议在读之前了解下Java中重量级锁的实现原理。

  1. 重量级锁的ObjectMonitor和JUC中的AQS有什么异同
  2. 为什么ObjectMonitor需要cxq和entryList两个等待队列
  3. cxq队列中等待线程,什么时候会进到EntryList
  4. 等待队列中多个线程,唤醒的顺序是什么
  5. 偏向锁和轻量级锁下线程是否可以wait和notify
  6. cxq和waitset数据结构有什么区别
  7. 被唤醒的wait线程和其它等待线程,谁会先抢到锁
  8. synchronized有类似AQS的公平锁/非公平锁逻辑吗

看了上面的问题,如果是胸有成竹,那就可以跳过这篇文章了。如果一脸问号,这篇文章应该对你有所帮助。

名词解释

首先明确下文章中用到的名词,防止引起误解。
 

等待队列,互斥锁实现中,当线程抢锁失败时,会被放入一个队列等待。当别的线程释放锁后会唤醒队列中的元素重新尝试抢锁,这个队列一般称为互斥等待队列,本文中称为等待队列。
同步队列,代码中调用wait方法时,当前线程会放入另外一个队列,等待其它线程notify,这个队列一般称为同步等待队列,本文中称为同步队列。

问题解析

问题1:ObjectMonitor和AQS有什么异同
ObjectMonitor和AQS(AbstractQueuedSynchronizer)都是依据管程模型的原理开发的。所以在整体架构上基本相同,都有共享变量和等待队列,在实现上又有区别。


1)共享变量,ObjectMonitor中使用owner做共享变量,通过CAS设置owner为当前线程来抢锁。而AQS中的共享变量是一个整形的status。因为这一区别,导致ObjectMonitor需要定义一个计数器来记录锁重入次数,而AQS需要额外定义个exclusiveOwnerThread来记录当前持有锁的线程。
2)等待队列,ObjectMonitor等待队列使用了两个队列,cxq和entryList,而AQS仅使用了一个等待队列。

AQS中的等待队列

1.同步等待队列
AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
AQS 依赖CLH同步队列来完成同步状态的管理:

  • 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
  • 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
  • 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)

2.条件等待队列
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:

  • 调用await方法阻塞线程;
  • 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)


3)条件同步,AQS支持在同一个锁上创建多个条件变量,wait/notify更加灵活和精准。而ObjectMonitor只有一个waitset,所有线程共享一个条件变量。
4)Share模式,AQS的Share模式可以使实现读写锁更加简单。

问题2: 为什么ObjectMonitor需要cxq和entryList两个等待队列
ObjectMonitor中加解锁、wait/notify都涉及对等待队列的进出队操作。如果使用一个队列冲突的概率会加大,耗费系统资源。分成2个队列后,出入队EntryList队列只有加锁的情况才会操作,不需要CAS和自旋,减少了资源消耗。

问题3:cxq队列中等待线程,什么时候会进到EntryList
抢锁线程在获取锁失败后,默认会进cxq队列。当持有锁的线程执行完释放锁时,会将cxq中的等待节点放入EntryList中。就是说cxq->EntryList这一步是锁释放之前的由持有锁的线程做的。

问题4:等待队列中多个线程,唤醒的顺序是什么
当持有锁的线程释放锁时,会先检查EntryList是否为空,如果不为空则唤醒EntryList中第一个节点。否则唤醒cxq中第一个节点。EntryList和cxq中出入队策略请看问题6。

问题5:偏向锁和轻量级锁下线程是否可以wait和notify
答案时是可以。原因很简单,因为wait/notify时是需要加入或者唤醒同步队列的,只有ObjectMonitor中才有同步队列。

问题6:cxq和waitset数据结构有什么区别

  1. cxq是一个双向链表,采用先进后出的策略,就是说后入队的线程将先获取到互斥锁,结构如下图:


    当前锁被其它线程持有,t0先尝试获取锁,t3最后尝试,cxq当前的状态如上图。最后入队的t3会排在第一位。当持有锁的线程解锁时,正常从队首出队,所以t3首先获得锁。

  2. waiset是一个回环链表,即尾节点的下一个节点是头节点,采用先进先出的策略。结构如下图:

    waitset


    问题7:notify/notifyAll后的线程和等待队列中线程,谁会优先抢到锁
    使用notify和notifyAll唤醒wait线程,jvm的处理是有区别的。
    1)如果是notify,唤醒的是waitset的队首节点,如果这时候EntryList不为空,则放入EntryList,否则放入cxq。无论是放入那个队列。因为是cxq后进先出,所以被唤醒的线程比等待队列中的线程先出队,会先抢到锁。
    2)如果是notifyAll,会将waitset中的所有节点逐个放入cxq中。按照问题4中的描述,如果EntryList不为空,则EntryList中首节点会先抢到锁,否则waitset中原最后一个节点先抢到锁,如下图所示:


    问题8:Synchronized有类似AQS的公平锁/非公平锁逻辑吗
    默认情况下,线程进入重量级锁的抢锁阶段,第一步就会尝试通过自旋来抢锁,所以默认相当于AQS中的非公平锁。即使自旋时未抢到锁,按照上面讲的cxq出入队逻辑,也是后进先出,正常情况下后进入等待队列的线程会先抢到锁,这一点也是和AQS中相反的。

hotspot中对于重量级锁的不同使用场景可以调整这个公平锁逻辑,但是不提供jvm启动参数,需要修改jvm的编译参数来实现。

总结

JVM中的Synchronized重量级锁逻辑和JDK中的AQS都是依据管程模型的理论来设计的,所以有诸多的相似之处。建议感兴趣的读者可以了解下管程模型,对于理解互斥锁会有很大帮助的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
无状态偏向锁、轻量级重量级都是Java中的机制,它们的实现方式和性能表现不同。 无状态:也称为自旋,当线程尝试获取时,如果已经被其他线程占用,该线程会一直自旋等待的释放,直到获取到为止。这种适用于的持有时间非常短的情况,因为长时间的自旋会浪费CPU资源。 偏向锁偏向锁是一种针对加操作的优化手段,它的目标是减少无竞争情况下的操作的性能消耗。当一个线程访一个偏向锁时,它会将对象头中的标识位设置为偏向,并将线程ID记录在对象头中。之后,该线程再次请求时,无需再次竞争,直接获取即可。这种适用于只有一个线程访对象的情况。 轻量级:轻量级是一种针对多线程竞争情况下的优化手段,它的目标是减少线程阻塞的时间,提高程序的并发性能。当一个线程访一个轻量级时,它会将对象头中的标识位设置为轻量级,并将对象的指针保存在线程的栈帧中。之后,其他线程再次请求时,会通过自旋的方式尝试获取,而不是阻塞等待。如果自旋失败,就会升级为重量级。这种适用于的竞争不是很激烈的情况。 重量级重量级是一种针对多线程竞争情况下的优化手段,它的目标是保证线程的正确性和程序的稳定性。当一个线程访一个重量级时,它会进入阻塞状态,直到被释放。这种适用于的竞争非常激烈的情况。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值