【JAVA核心知识】20:synchronized实现原理与锁膨胀:无锁or偏向锁-轻量级锁-重量级锁,看完就懂

本文详细介绍了Java中synchronized的优化,包括无锁、偏向锁、轻量级锁和重量级锁的概念及其膨胀过程。重点讲解了偏向锁的优化策略,如批量重偏向和批量锁撤销,以及轻量级锁的加锁和释放流程。此外,还探讨了锁的非公平性和闯入线程的现象。
摘要由CSDN通过智能技术生成

1. 前言

每一个重量锁对象都有且仅有一个monitor对象,重量级锁加锁的本质就是在竞争monitor对象。如果一个线程竞争monitor失败,锁逻辑会让这个线程进入阻塞状态,而线程的阻塞与唤醒需要进行内核态和用户态之间的转换,这是一个代价颇大的动作,这就是为什么重量锁性能差的原因。
synchronized是典型的重量级锁。但是JDK1.6对synchronized的优化使得synchronized不再只有重量级锁一个模式。
synchronized可以把任意一个非NULL的对象当作锁,他是一个独占式的悲观锁,也是可重入锁,也是一个非公平锁。

2. synchronized的作用范围

  1. 作用于方法时,锁住的是对象的实例(this)。该对象下所有同步方法共用该对象锁(this)。
  2. 作用于静态方法时,锁住的时Class实例,又因为Class实例存储在方法区内全局共享,因此静态方法锁相当于一个全局锁,会锁住所有调用该方法的线程。
  3. 作用于对象实例时,锁住的是所有以该对象为锁的代码块。使用此方式时要注意,尽量将对象实例设置为final的,因为synchronized是针对堆中实际对象的,如果在synchronized执行过程对象实例被重新赋值,那么就可能导致即使在对同一对象进行操作,不同的线程也锁定在不同的对象上,达不到同步目的。

3. synchronized 的进入

synchronized加锁分为代码块加锁和方法加锁。
代码块加锁是通过代码块前的monitorenter加锁字节码,代码块后的monitorexit解锁字节实现的。方法加锁是通过在方法上加一个标记位ACC_SYNCHRONIZED 实现的,JVM发现此标志位后在执行对应方法就会进入加锁逻辑。
两种方式本质上是一致的。只是ACC_SYNCHRONIZED是隐式的,没有直接通过字节码进入加锁逻辑。

4. JDK对synchronized的优化-锁膨胀

因为重量级锁的性能问题,JDK也在一直对synchronized进行优化,最具有突破性的是JDK1.6对synchronized进行的优化,JDK1.6之前synchronized只有无锁-有锁(重量级锁)两个状态,JDK1.6将synchronized由轻到重分为无锁or偏向锁-轻量级锁-重量级锁 4个状态,根据实际情况对锁进行膨胀,锁膨胀不可逆,只能由轻变重。使得synchronized变得不那么重,大大提升了synchronized的性能。
值得注意的是wait/notify方法的实现依赖于monitor,同步代码块一旦调用wait/notify就会直接进入重量级锁模式。至于为什么咱们等到下面重量级锁章节解释。
可以通过设置JVM参数UseHeavyMonitors禁用偏向锁和轻量锁,直接使用重量锁。
在了解锁膨胀之前先了解一下锁膨胀实现的关键:位于Java对象头中的Mark Word.
Mark Word
上图即为堆中一个对象组成的简单结构,Mark Word所处的位置已用蓝色标出,灰色部分与锁无关。JAVA对象头一般由两个机器码组成,Mark Word和类型指针各占据一个,如果是数组对象则会多出一个机器码用来维护数组长度以便JVM确认数组大小。
Mark Word用来存储对象运行时数据,如identityHashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向ID等。以32位JVM为例,一个机器码4个字节,即32bit。32bit自然无法存储Mark Word要存的所有内容,因此会复用存储空间,对象处于不同的状态,Mark Word内存的内容也会不同,不同状态下存储的内容如图:
在这里插入图片描述

其中需要重点关注的地方用蓝色底色标出,结合这个图片能更好的理解锁膨胀过程中的各个阶段。

5. 无锁

无锁状态顾名思义,未进行任何锁定。注意无锁和偏向锁是同一级别的!他们之间不存在膨胀关系!从上面的Mark Word图可以看到无锁是0(偏向标志位)01(锁标志位),偏向锁是1(偏向标志位)01(锁标志位)。如果一个对象允许偏向锁,那么他被new出来就是101,处于匿名偏向状态。如果一个对象不允许偏向锁,那么他被new出来就是001,处于无锁状态。是否允许偏向锁由JVM参数与Class内的允许偏向标识共同决定。锁对象不能从无锁状态膨胀到偏向锁状态,无锁状态一旦产生加锁动作就会直接进入轻量锁或重量锁的加锁流程,可以理解为无锁状态是轻量锁的空闲状态,意为当前没有线程持有锁。重量级锁的无锁状态不是通过Mark Word的状态判断的,而是通过monitor的owner是否为null判定的。
目前网络上有很多文章都描述为对象初始是无锁状态,第一个线程获取锁时膨胀为偏向锁状态,偏向锁撤销也是回到无锁状态。如果你也这样认为的话可以参考一下这篇Synchronized你以为你真的懂?,文中打印出了初始状态下的Mark Word,偏向延迟时间内不允许使用偏向锁对象初始状态即为001(无锁),进行加锁直接膨胀为000(轻量级锁)。过了偏向延迟时间后允许使用偏向锁对象初始状态初始即为101(偏向锁),处于匿名偏向状态。但是此文中的锁膨胀流程图依旧错误标出了无锁可膨胀为偏向锁的路径。另外这篇死磕Synchronized底层实现–偏向锁从源码的角度剖析了偏向锁的获取,膨胀及撤销的流程。当然如果你有不同的意见,欢迎评论区讨论。
下图是锁膨胀文章经常引用的一张图,很清晰了标出了锁的膨胀的途径:
锁膨胀

6. 偏向锁

偏向锁是为了提高只有一个线程锁定资源时的性能。轻量级锁是为了在线程交替锁定资源时提高性能。继续阅读之前请记住这句话,有利于理解偏向锁的膨胀时机,所有偏向锁膨胀为轻量级锁的场景都契合这句话。
JDK团队对synchronized的优化过程中发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除线程重入(CAS)的开销,看起来是让这个线程得到了偏袒。引入偏向锁是为了在无多线程竞争的场景下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS指令,而偏向锁只需要在获取时或切换时依赖一次CAS指令。

6.1 匿名偏向

上文无锁篇已经提过:如果一个对象允许偏向锁,那么他被new出来就是101,处于匿名偏向状态。匿名偏向状态是偏向锁的初始状态,此时其标识位为101意为偏向锁状态,但是实际上此时他却没有偏向任何线程,记录偏向ID处的值为0。
偏向锁撤销后也是回到匿名偏向状态重新竞争。

6.2 偏向锁的获取流程

线程获取锁时,首先会判断是否有空闲的Lock Record,有空闲的才会进行走加锁逻辑,否则就重复执行加锁直到有空闲的Lock Record。至于Lock Record是什么等到轻量级锁再说,他是轻量级级锁实现的关键,这里知道有这么个判断就可以了。
在这里插入图片描述
不同于其他锁,执行完毕就要释放锁,偏向锁的释放是在下一个线程竞争时才执行的锁撤销。
另外值得一提的是偏向锁的CAS操作并不是针对Mark Word中的某一个值进行替换,而是构建一个新的Mark Word替换旧的Mark Word。可见这篇死磕Synchronized底层实现–偏向锁的源码解析部分。

6.3 偏向锁的优化:批量重偏向与批量锁撤销

6.3.1 批量重偏向

锁撤销需要到安全点才可以进行。但是如果一个代码块需要大量的加锁操作,就会导致一个线程持有大量的偏向锁,那么在其他线程执行这个代码块获取锁时就可能在安全点进行大量的锁撤销,这会使得偏向锁的性能急剧变差。为了应对这种情况,JVM在进行锁撤销时带有一个批量重偏向的优化机制。
在上面的Mark Word图中可以看到偏向锁状态时锁对象会用两个字节记录epoch,可以简单的认为这个epoch就是一个时间戳,除此之外锁对象对应的Class内部也维护一个epoch。一个新对象被创建时,Mark Word中的epoch和Class的epoch保持一致。
以Class为单位,每个Class会维护一个偏向锁撤销计数器,属于该Class的锁对象每进行一次锁撤销,Class内维护的计数器就+1,当达到阈值时(默认20,由JVM参数BiasedLockingBulkRebiasThreshold控制),就认为这个Class的所属的锁对象出现了问题,进行批量重偏向。
进行批量重偏向时,Class会生成一个新的epoch_new,并扫描所有该Class所属的对象,如果对象处于偏向状态且偏向线程运行在同步代码块,则将Mark Word中的epoch修改为epoch_new,如果偏向线程已经死亡或不在同步代码块内则不变,以此来确认该Class所属的对象哪些处于正常偏向,哪些已经处于偏向失效状态。之后其他线程获取这些锁对象时如果锁对象的epoch和Class中的epoch一样的话则走正常的偏向锁获取流程,不一样的话就能知道这个偏向锁已经失效,无需等待安全点可以直接通过CAS替换旧的Mark Word获取偏向锁。
注意锁撤销的次数计数是以Class为维度计算的,也就是某个Class的所有锁实例的撤销次数和达到阈值就会对该Class下的所有锁对象进行批量重偏向操作,而不是以单一一个对象为维度的。如:Lock a = new Lock();Lock b = new Lock(); 那么锁撤销次数=a的撤销次数+b的撤销次数。

6.3.2 批量锁撤销

进行批量重偏向后,撤销次数计数器依然会继续计数,如果锁撤销时发现撤销计数器达到40时(由JVM参数BiasedLockingBulkRevokeThreshold控制),就会进行批量锁撤销,JVM会在安全点将该Class所有锁实例的偏向锁全部撤销,膨胀为轻量级锁,并标记为禁止偏向锁,关闭该Class的偏向锁功能(Class内部有一个是否允许偏向锁的标识),上文也说过是否允许偏向锁由JVM参数与Class内的允许偏向标识共同决定。后续new出来的该Class实例会直接被禁用偏向锁,其他Class则不会受影响。
批量锁撤销也是以Class为维度,而不是以对象为维度。
批量锁撤销存在的意义就是频繁的锁撤销意味着线程在交替执行,显然更适合轻量级锁。

6.4 锁定时间

由JVM参数BiasedLockingDecayTime控制,默认为25000。即批量锁偏向之后超过25000ms还没有达到批量锁撤销的阈值,就重置撤销次数计数器。毕竟批量锁偏向和批量锁撤销都是为了应对短时间内线程的频繁切换而做的优化,如果两次线程切换时间段相隔很长,就是一次正常的锁撤销了。

6.5 偏向锁延迟

由JVM参数BiasedLockingStartupDelay控制,默认为4,即JVM启动的前4s不启用偏向锁,这是因为JVM刚启动时竞争比较激烈,并不适合偏向锁,因此跳过这一阶段。

6.6 关闭偏向锁

由JVM参数UseBiasedLocking控制,JDK1.6之后默认为true,即启用。

6.7 identityHashCode对偏向锁的影响

如果你没有重写对象的HashCode方法,使用的是Object的native int hashCode(),那么返回的hashCode就是identityHashCode,即使重写了HashCode,也可以通过System.identityHashCode(Object x)获得identityHashCode。也许你可以重写hashCode()方法让对象在不同状态返回不同的Hash值,但是一个对象的identityHashCode一经计算就永远不会改变,无锁状态时MarkWord中存的就是identityHashCode,identityHashCode会在第一次计算时存入MarkWord,后续再获取identityHashCode就会直接返回这个,而重写的HashCode并不会存入MarkWord。这也是为什么都说对象的identityHashCode和地址相关,但是明明对象会因为GC而移动改变存储位置,返回的identityHashCode却永远相同,就是因为identityHashCode只与它第一次计算时的地址有关,后续返回的都是存在MarkWorde的identityHashCode值。
通过上面的MarkWord存储内容图,可以看到无锁状态下identityHashCode和偏向状态下偏向线程ID+Epoch的位置是重叠的,这意味着同时只能存在一个,因此如果线程处于偏向锁状态时如果计算了identityHashCode就必须退出偏向状态。计算identityHashCode时(无论是通过Object.hashcode()还是System.identityHashCode(Object x))发现Object处于偏向状态,就会检测是否处于加锁状态即是否在同步代码块中,如果不处于加锁状态则退化为无锁状态并存储identityHashCode,如果处于加锁状态,则会直接膨胀为重量级锁。值得注意的是匿名偏向也是不处于的一种,因此如果创建一个对象是匿名偏向状态,然后计算了identityHashCode,这个对象就会变成无锁状态,再加锁会进入轻量锁的逻辑。 概括起来一句话,一个对象一旦计算了identityHashCode,那么这个对象就和偏向锁无缘了。值得一提的是Object默认的toString方法也会调用hashCode()方法。
那为什么轻量级锁和重量级锁和identityHashCode存储位置重合的部分也存的有其他内容,他们却可以呢?这个是因为轻量级锁的Lock Record中的Displaced Mark word记录的就是对象无锁状态时的MarkWord,而重量级锁的ObjectMonitor中也有属性存储MarkWord的相关信息。也就是轻量级锁和重量级锁把对象的identityHashCode存在了其他位置。为什么偏向锁不这么做,因为偏向锁面向的就是较为简单的场景,没有必要这样做变的更复杂起来,而且一般情况下加锁对象也不会计算identityHashCode,当然是越简单越好。

6.8 举个例子

有一群猴子(锁对象)和一个猴王(Class),在庆功宴上论功行赏,作为奖品的苹果(第一个线程)先端上来了,猴子们都选择了苹果(偏向锁偏向第一个线程),等了一会又端上来了一批香蕉(第二个线程),一批西瓜(第三个线程)作为奖品,这时间有的猴子就愿意选香蕉或者西瓜了,就去找猴王换,猴王记着换的次数,到20次的时间,猴王就想既然你们这么多要换奖品的,那么干脆,苹果还完整的没吃过的(偏向线程已经死亡的或未处于同步代码块的)猴子,全部给你们一次换奖品的机会,你们不用找我了,要换的直接用苹果换新奖品(CAS替换偏向Mark Word),不愿意换的还拿着就行。结果执行了这个政策之后,还频繁的有猴子找猴王换奖品,换到40次时,猴王想,这样不行呀,还有这么多猴子换,看来大家都比较摇摆,那就就直接升级成轻量级锁吧,新的猴子也不让他们自己选了,直接用轻量级锁。最后猴王是健忘症,他的记忆中只有25000ms(锁定时间),过了这个点他的计数就会变成0。

7. 轻量级锁

轻量级锁的“轻量”是相较于使用monitor依赖于操作系统的重量级锁而言的。轻量级锁并不能代替重量级锁,轻量级锁是为了在没有多线程竞争的场景下,避免重量级锁的性能消耗。轻量级锁适用的场景是线程交替执行同步块的情况,如果出现同一时间竞争同一锁的情况,轻量级锁就会膨胀为重量级锁。

7.1 轻量级锁如何加锁

线程进行加锁时,JVM会在当前线程的栈中找到一个空闲的名为锁记录的空间,锁记录被称为Lock Record。偏向锁部分判断线程是否在执行同步代码块就是通过这个偏向线程栈中的Lock Record判断的。Lock Record由两部分组成,一部分用来存储Mark Word,这个Mark Word被称为Displaced Mark word,另一部分用来存储指向锁对象的指针,以此来确定这个Lock Record属于哪个锁。
还记得上面Mark Word的存储内容图吗?轻量锁的Mark Word除了锁标志位外还会存储一个指向锁记录的指针。这是因为轻量级锁加锁时会在线程栈中放入一个Displaced Mark word为无锁的状态的Lock Record,然后构造一个指向这个Lock Record的轻量级锁Mark Word并通过CAS替换锁对象中的无锁Mark Word。如果成功就意味着加锁成功了。
但是如果是锁重入呢?那么此时仅仅会构造一个Displaced Mark word为null的Lock Record放入到持有锁的线程栈中以进行重入计数,以保证解锁次数和加锁次数相同时才真正的释放锁。至于为什么不把锁重入次数计入锁对象的Mark Word呢,因为Mark Word容量有限呀。
加完锁大概就是这个样子:
在这里插入图片描述

7.2 轻量级锁加锁流程

轻量级锁加锁有两个来源,一个是从无锁状态来的,一个是偏向锁膨胀成轻量级锁的:
在这里插入图片描述

7.3 轻量锁的释放

遍历线程栈中的Lock Record,如果Lock Record的Obj指向锁对象,说明这个Lock Record属于此锁的锁记录,如果发现线程栈中含有Displaced Mark word为null的Lock Record那么就说明这次锁释放是一次重入锁释放,此时仅仅需要将这个Lock Record的Obj设置为null,即释放了这个锁记录,也就是一次重入锁释放。如果仅剩最后一个Displaced Mark word为无锁状态的Lock Record,那么同样将这个Lock Record的Obj设置为null,然后通过CAS将这个Lock Record的Displaced Mark word替换到对象头的Mark Word中,使得锁对象重新回到无锁状态,轻量锁就释放完毕了。

8. 重量级锁

重量级锁的实现依靠monitor,每一个锁对象都有且仅有一个自己的monitor对象。因此synchronized膨胀为重量级锁的第一件事情就是申请一块内存获取自己的ObjectMonitor,并且初始化。
ObjectMonitor的初始化需要一个过程,而重量级锁的膨胀一般处于竞争比较激烈的阶段。为了避免ObjectMonitor的重复申请与初始化,会记录一个膨胀状态到Mark Word中,如果已经有线程在膨胀了,那么后面的线程就会自旋等待膨胀完成。还记Mark Word的重量锁结构吗,除了锁标志位10外还会记录一个指向monitor的指针。膨胀完成之后Mark Word就会成为重量锁状态,然后指针就指向这个初始化完成的ObjectMonitor。后续的线程通过这个指针就可获得此锁对象对应的ObjectMonitor。

8.1 ObjectMonitor的核心组件

  1. Owner:一个[void*]无类型指针,指向当前持有锁的线程(Thread)。所谓的竞争锁就是通过CAS把ObjectMonitor的Owner指向自己(Thread),ObjectMonitor中的Owner指向谁就是谁持有锁,为null时就是空闲状态。持有锁的线程被称为Owner线程。
  2. Wait Set:一个[ObjectWaiter *]类型的指针,双向链表。用来存放那些调用了wait()方法被阻塞的线程。只有重量级锁模式下才会通过monitor维护这样一个wait的集合以便于notify时进行唤醒。这也是为什么同步代码块一旦调用wait/notify就会直接进入重量级锁模式以及wait/notify需要在同步代码块内执行,否则会抛出IllegalMonitorStateException的原因。
  3. cxq(又名Contention List):一个[ObjectWaiter *]类型的指针,单向链表。竞争队列,存放那些获取锁失败后进入阻塞状态等待锁资源释放的线程。通过CAS放在cxq的首部。如果因为竞争导致载入失败就自旋继续放。
  4. Entry List:一个[ObjectWaiter *]类型的指针,双向链表。候选队列,存放那些有资格成为候选线程的线程。Entry List是和cxq类似的。cxq本身首部就竞争比较激烈,如果OnDeck线程继续从cxq首部取会有更大的竞争。导致cxq首部会有大量的CAS访问。Entry List的意义便是为了在竞争激烈的场景下降低cxq的首部竞争。
  5. OnDeck(又名Heir presumptive):假定继承线程,当锁释放后从Entry List取出的进行锁竞争的线程。注意是指从Entry List中取出的参与竞争的线程,一个假定继承的线程。闯入线程[注]虽然参与竞争,但并不是OnDeck。

闯入线程:synchronized是非公平锁,并不会严格的按照先来后到的顺序依次获取锁,闯入线程是指锁释放后OnDeck线程未获取锁之前,出现的一个新的获取锁的线程。此时他会直接尝试获取锁,也就是和从Entry List取出的OnDeck竞争锁,竞争成功OnDeck线程重新阻塞,竞争失败闯入线程就会作为新的阻塞线程进入cxq。

8.2 线程在组件间的流转

8.2.1 默认策略

在这里插入图片描述

  1. 线程直接尝试获取锁,获取成功则持有锁成为Owner执行同步代码块,获取失败则进入cxq首位,并转为内核态阻塞。
  2. Owner线程执行完同步代码块释放锁,并取出Entry List的首位指定为OnDeck线程。如果Entry List为空则执行(3).
  3. 如果cxq为空,则自旋执行(2)。否则将cxq中的元素载入Entry List。具体做法是先用局部变量w指向cxq,然后将cxq置为null,这样就相当于清空了cxq,以继续接收新的阻塞线程。最后将w赋值给Entry List,不过因为cxq是单向链表,Entry List是双向链表,所以这一步还会遍历各个节点,赋值各个节点的prev进行单向链表到双向链表的转换,然后自旋执行(2)。
  4. OnDeck线程竞争锁,如果竞争成功则从Entry List首位移除,如果因为闯入线程的原因竞争失败则重新进入阻塞状态。
  5. Owner线程执行wait()方法,Ower线程阻塞进入Wait Set的首位。
  6. Owner线程执行notify()方法,取出Wait Set的首位放入cxq的首位。notifyAll()相当于循环调用notify()。

通过上面的流转步骤,我们可以得出以下几个点:
非公平锁:结合步骤1,可以看到对于synchronized,新竞争线程会直接尝试获取锁,而不会管锁处于什么状态,是否已经有线程在等待,获取不到才会阻塞等待。这就是非公平锁的特点。
阻塞唤醒顺序:结合步骤2,3。可以得到默认策略下,阻塞进入cxq的顺序是A-B-C,那么迁移如Entry List后被取出的顺序是C-B-A,这意味着解锁顺序也是C-B-A。且此种策略每次迁移均是迁移当前cxq中所有的数据,也就是说如果在迁移过程中有线程D进入cxq,那么D会在下一次迁移,而不是在当前迁移批次。如果没有闯入线程的情况下,发生以下情况:A阻塞-B阻塞-C阻塞-迁移-D阻塞-E阻塞。那么解锁顺序应该C-B-A-E-D。
竞争切换:结合步骤4。可以看到OnDeck线程是需要重新竞争锁的,这样虽然牺牲了一些公平性,但是却能极大的提升系统的吞吐量。这是因为OnDeck线程从阻塞到运行需要进行内核态到用户态的切换,这是一个很耗时的过程,如果直接把锁指定给OnDeck线程,那么在OnDeck切换状态的期间就会处于无线程实际运行的情况,而在这个过程中允许闯入线程获取锁就能很好的利用这一段空白时间。这也是为什么非公平锁比公平锁性能高的原因。在JVM中,把这种模式的选择行为称之为"竞争切换"。
wait/notify唤醒顺序:结合步骤5.6。可以得到默认策略下,先执行wait()方法的线程会被后唤醒。也就是说执行wait()方法的顺序是A-B-C,那么notify()方法的唤醒顺序是C-B-A。但是注意这并不意味着一定是C会先拿到锁,因为notify仅仅是将线程放到了cxq的首部,而阻塞唤醒顺序也是逆向的。如果C-B-A的唤醒均在下一次cxq迁移之前完成,这样就是A会先拿到锁了。

8.2.2 其他策略

除了上面的默认流转策略外。流转过程中阻塞唤醒顺序和wait/notify唤醒顺序都是可变的。阻塞唤醒顺序通过Knob_QMode 控制策略。wait/notify唤醒顺序通过Knob_MoveNotifyee控制。这里就不再画图了,仅仅列举一下这两处地方采用不同策略的区别:
Knob_QMode控制阻塞唤醒顺序
Knob_QMode 有5个值,分别为0,1,2,3,4,默认策略为0。以下策略都是自旋的,一直会自旋到取到线程为止:

  • 0:默认策略。OnDeck取Entry List的首位。如果Entry List为空则先用局部变量w指向cxq,然后将cxq置为null,最后将w改造成新的Entry List,再取Entry List的首位成为OnDeck线程。
  • 1:OnDeck取Entry List的首位。如果Entry List为空则将cxq的节点逆向载入Entry List,再取Entry List的首位成为OnDeck线程。
  • 2:OnDeck直接取cxq的首位。如果cxq为空则取EntryList的首位。
  • 3:将cxq的元素插入Entry List的队尾(cxq的首接Entry List的首),再取Entry List的首位成为OnDeck线程。
  • 4:将cxq的元素插入Entry List的队首(cxq的尾接Entry List的首),再取Entry List的首位成为OnDeck线程。

Knob_MoveNotifyee控制wait/notify唤醒顺序
Knob_MoveNotifyee有4个值,分别为0,1,2,3,默认策略为2

  • 0:载入到Entry List的首位
  • 1:载入到Entry List的尾部
  • 2:默认策略,如果Entry List为空载入Entry List,否则载入到cxq的首位
  • 3:载入到cxq的尾部

8.3 synchronized重量级锁模式的加锁流程

在这里插入图片描述

9. 结语

synchronized竞争与膨胀过程并不像本文描述的这样简单,本文仅仅展现出竞争与膨胀过程的关键节点。更详细的了解可以通过源码完成。这里推荐死磕Synchronized底层实现–偏向锁死磕Synchronized底层实现–轻量级锁死磕Synchronized底层实现–重量级锁三篇文章,在写这篇博文中过程中给予了我很多的帮助。另外Synchronized 轻量级锁会自旋?好像并不是这样的。jdk源码剖析二: 对象内存布局、synchronized终极原理也进行了参考。

PS:
【JAVA核心知识】系列导航 [持续更新中…]
上篇导航:19:JAVA中的各种锁
下篇预告:21:从源码看ReentrantLock
欢迎关注…

参考资料:
死磕Synchronized底层实现–偏向锁
死磕Synchronized底层实现–轻量级锁
死磕Synchronized底层实现–重量级锁
Java的wait()、notify()学习三部曲之二:修改JVM源码看参数
Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)
Synchronized 轻量级锁会自旋?好像并不是这样的
jdk源码剖析二: 对象内存布局、synchronized终极原理
Hotspot 重量级锁ObjectMonitor(一) 源码解析
Hotspot 重量级锁ObjectMonitor(二) 源码解析
Synchronized你以为你真的懂?
深入分析Synchronized原理(阿里面试题)
JAVA锁的膨胀过程和优化(阿里)
深入理解synchronized底层原理,一篇文章就够了!
synchronized底层实现原理&CAS操作&偏向锁、轻量级锁,重量级锁、自旋锁、自适应自旋锁、锁消除、锁粗化
关于jvm:偏向锁的批量重偏向与批量撤销机制
偏向锁

附录-非正文

写这篇博文时的一些验证性代码,简单记录:
非公平与闯入线程验证:

public class LockIsNotFair {

    public static void main(String[] args) {
        Object lock = new Object();
        
        for (int i = 0; i < 1000; i++) {
            final int j = i;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
            Thread t = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    synchronized (lock) {
                        System.out.println(j + "争到锁");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t.start();
        }
    }

}

运行结果:

0争到锁
9争到锁
8争到锁
7争到锁
6争到锁
5争到锁
4争到锁
3争到锁
2争到锁
1争到锁
89争到锁
99争到锁    // 这个99是一个很明显的闯入线程
88争到锁
87争到锁
86争到锁
85争到锁
...

QMode的默认策略:

public static void main(String[] args) {

   Object lock = new Object();
   
   for (int i = 0; i < 100; i++) {
	   try {
		   Thread.sleep(10);
	   } catch (InterruptedException e1) {
		   e1.printStackTrace();
	   };
	   final int j = i;
	   new Thread(new Runnable() {
		   
		   @Override
		   public void run() {
			   System.out.println(j + "进入");
			   synchronized (lock) {
				   System.out.println(j + "拿到锁");
				   try {
					   Thread.sleep(100);
				   } catch (InterruptedException e) {
					   e.printStackTrace();
				   }
				   System.out.println(j + "释放锁");
			   }
			   
		   }
	   }).start();
   }

}

运行结果:

0启动竞争
0拿到锁
1启动竞争
2启动竞争
...
9启动竞争
0释放锁
9拿到锁 //cxq移入了EntryList
...
8拿到锁
...
7拿到锁
...
...
1拿到锁
...
91启动竞争
92启动竞争
1释放锁
92拿到锁 //cxq移入了EntryList
93启动竞争

WaitSet唤醒的默认策略:

package sync.condition;

public class WaitSetDemo {

    public static void main(String[] args) {
        Object lock = new Object();

        for (int i = 0; i < 200; i++) {
            final int j = i;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
            Thread t = new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println(j + "开始竞争");
                    synchronized (lock) {
                        System.out.println(j + "争到锁1");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        if (j == 6) {
                            try {
                                System.out.println(j + "wait");
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println(j + "争到锁2");
                        }
                        if (j == 4) {
                            System.out.println(j + "notifyAll"); 
                            lock.notifyAll();
                        }
                    }
                    System.out.println(j + "释放锁");
                }
            });
            t.start();
        }

    }

}

运行结果:

46开始竞争
47开始竞争
6wait
5争到锁1
48开始竞争
49开始竞争
...
65开始竞争
66开始竞争
3争到锁1
4notifyAll // 4在66进入cxq后,67进入cxq之前notifyAll
4释放锁
67开始竞争
68开始竞争
...
69释放锁
68争到锁1
68释放锁
67争到锁1
67释放锁
6争到锁2  // 6在66后,67前重新获得锁
6释放锁
66争到锁1
65争到锁1
66释放锁
...
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yue_hu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值