开篇语 Synchronized,Java友好的提供了的一个关键字,它让开发者可以快速的实现同步。它就像一个星星,远远看去就是一个小小的点。但是走近一看,却是一个庞大的蛋糕。而这篇文章就是要将这个巨大的蛋糕切开,吃进肚子里面去。Synchronized使用 在Java中,如果要实现同步,Java提供了一个关键词synchronized来让开发人员可以快速实现同步代码块。publicclassTest{publicstaticvoidmain(String〔〕args){ObjectonewObject();Threadthread1newThread((){synchronized(o){System。out。println(获取锁成功);}})。start();}}复制代码 线程thread1获取对象o的锁,并且输出一句话获取锁成功。publicclassTest{privateinti0;publicsynchronizedvoidset(inti){this。ii;}publicsynchronizedstaticStringget(){return静态方法;}publicvoidput(){synchronized(this){System。out。println(同步代码块);}}}复制代码 synchronized关键字除了可以用于代码块,还可以用于方法上。用于实例方法上时,线程执行该方法之前,会自动获取该对象锁,获取到对象锁之后才会继续执行实例方法中的代码;用于静态方法上时,线程执行该方法之前,会自动获取该对象所属类的锁,获取到类锁之后才会继续执行静态方法中的代码。用于代码块上时,可以传入任意对象作为锁,并且可以控制锁的粒度。synchronized实现原理 下面是Test类的字节码文件publicclassTestminorversion:0majorversion:55flags:(0x0021)ACCPUBLIC,ACCSUPERthisclass:7Testsuperclass:8javalangObjectinterfaces:0,fields:1,methods:4,attributes:1Constantpool:1Methodref8。27javalangObject。init:()V2Fieldref7。28Test。i:I3String29静态方法4Fieldref30。31javalangSystem。out:LjavaioPrintStream;5String32同步代码块6Methodref33。34javaioPrintStream。println:(LjavalangString;)V7Class35Test8Class36javalangObject9Utf8i10Utf8I11Utf8init12Utf8()V13Utf8Code14Utf8LineNumberTable15Utf8LocalVariableTable16Utf8this17Utf8LTest;18Utf8set19Utf8(I)V20Utf8get21Utf8()LjavalangString;22Utf8put23Utf8StackMapTable24Class37javalangThrowable25Utf8SourceFile26Utf8Test。java27NameAndType11:12init:()V28NameAndType9:10i:I29Utf8静态方法30Class38javalangSystem31NameAndType39:40out:LjavaioPrintStream;32Utf8同步代码块33Class41javaioPrintStream34NameAndType42:43println:(LjavalangString;)V35Utf8Test36Utf8javalangObject37Utf8javalangThrowable38Utf8javalangSystem39Utf8out40Utf8LjavaioPrintStream;41Utf8javaioPrintStream42Utf8println43Utf8(LjavalangString;)V{publicTest();descriptor:()Vflags:(0x0001)ACCPUBLICCode:stack2,locals1,argssize10:aload01:invokespecial1MethodjavalangObject。init:()V4:aload05:iconst06:putfield2Fieldi:I9:returnLineNumberTable:line5:0line7:4LocalVariableTable:StartLengthSlotNameSignature0100thisLTest;publicsynchronizedvoidset(int);descriptor:(I)Vflags:(0x0021)ACCPUBLIC,ACCSYNCHRONIZEDCode:stack2,locals2,argssize20:aload01:iload12:putfield2Fieldi:I5:returnLineNumberTable:line10:0line11:5LocalVariableTable:StartLengthSlotNameSignature060thisLTest;061iIpublicstaticsynchronizedjava。lang。Stringget();descriptor:()LjavalangString;flags:(0x0029)ACCPUBLIC,ACCSTATIC,ACCSYNCHRONIZEDCode:stack1,locals0,argssize00:ldc3String静态方法2:areturnLineNumberTable:line14:0publicvoidput();descriptor:()Vflags:(0x0001)ACCPUBLICCode:stack2,locals3,argssize10:aload01:dup2:astore13:monitorenter4:getstatic4FieldjavalangSystem。out:LjavaioPrintStream;7:ldc5String同步代码块9:invokevirtual6MethodjavaioPrintStream。println:(LjavalangString;)V12:aload113:monitorexit14:goto2217:astore218:aload119:monitorexit20:aload221:athrow22:returnExceptiontable:fromtotargettype41417any172017anyLineNumberTable:line18:0line19:4line20:12line21:22LocalVariableTable:StartLengthSlotNameSignature0230thisLTest;StackMapTable:numberofentries2frametype255fullframeoffsetdelta17locals〔classTest,classjavalangObject〕stack〔classjavalangThrowable〕frametype250chopoffsetdelta4}复制代码 我们通过查看字节码可以发现,synchronized关键字作用在实例方法和静态方法上时,JVM是通过ACCSYNCHRONIZED这个标志来实现同步的。而作用在代码块时,而且通过指令monitorenter和monitorexit来实现同步的。monitorenter是获取锁的指令,monitorexit则是释放锁的指令。对象头 通过上文我们已经知道,Java要实现同步,需要通过获取对象锁。那么在JVM中,是如何知道哪个线程已经获取到了锁呢? 要解释这个问题,我们首先需要了解一个对象的存储分布由以下三部分组成:对象头(Header):由MarkWord和KlassPointer组成实例数据(InstanceData):对象的成员变量及数据对齐填充(Padding):对齐填充的字节 MarkWord记录了对象运行时的数据:identityhashcode:哈希码,只要获取了才会有age:GC分代年龄biasedlock:1表示偏向锁,0表示非偏向锁lock锁状态:01无锁偏向锁;00轻量级锁;10重量级锁;11GC标志偏向线程ID 128bit(对象头) 状态 64bitMarkWord 64bitKlassPoiter unused:25 identityhashcode:31 unused:1 age:4 biasedlock:1 lock:2 无锁 threadId:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 偏向锁 ptrtolockrecord:62 lock:2 轻量级锁 ptrtoheavyweightmonitor:62 lock:2 重量级锁 lock:2 GC标记 当线程获取对象锁的时候,需要先通过对象头中的MarkWord判断对象锁是否已经被其他线程获取,如果没有,那么线程需要往对象头中写入一些标记数据,用于表示这个对象锁已经被我获取了,其他线程无法再获取到。如果对象锁已经被其他线程获取了,那么线程就需要进入到等待队列中,直到持有锁的线程释放了锁,它才有机会继续获取锁。 当一个线程拥有了锁之后,它便可以多次进入。当然,在这个线程释放锁的时候,那么也需要执行相同次数的释放动作。比如,一个线程先后3次获得了锁,那么它也需要释放3次,其他线程才可以继续访问。这也说明使用synchronized获取的锁,都是可重入锁。字节序 我们知道了对象头的内存结构之后,我们还需要了解一个很重要的概念:字节序。它表示每一个字节之间的数据在内存中是如何存放的?如果不理解这个概念,那么在之后打印出对象头时,也会无法跟上述展示的对象头内存结构相互对应上。 字节序:大于一个字节的数据在内存中的存放顺序。 注意!注意!注意!这里使用了大于,也就是说一个字节内的数据,它的顺序是固定的。大端序(BIGENDIAN):高位字节排在内存的低地址处,低位字节排在内存的高地址处。符合人类的读写顺序小端序(LITTLEENDIAN):高位字节排在内存的高地址处,低位字节排在内存的低地址处。符合计算机的读取顺序 我们来举个例子: 有一个十六进制的数字:0x123456789。 使用大端序阅读:高位字节在前,低位字节在后。 内存地址 1hr2hr3hr4hr5hr十六进制 0x01 0x23 0x45 0x67 0x89 二进制 00000001hr00100011hr01000101hr01100111hr10001001hr使用小端序阅读:低位字节在前,高位字节在后。 内存地址 1hr2hr3hr4hr5hr十六进制 0x89 0x67 0x45 0x23 0x01 二进制 10001001hr01100111hr01000101hr00100011hr00000001hr既然大端序符合人类的阅读习惯,那么统一使用大端序不就好了吗?为什么还要搞出一个小端序来呢? 这是因为计算机都是先从低位开始处理的,这样处理效率比较高,所以计算机内部都是使用小端序。其实计算机也不知道什么是大端序,什么是小端序,它只会按顺序读取字节,先读第一个字节,再读第二个字节。Java中的字节序 我们可以通过下面这一段代码打印出Java的字节序:publicclassByteOrderPrinter{publicstaticvoidmain(String〔〕args){System。out。println(ByteOrder。nativeOrder());}}复制代码 打印的结果为:LITTLEENDIAN。 因此,我们可以知道Java中的字节序为小端字节序。如何阅读对象头 在理解了字节序之后,我们来看看如何阅读对象头。 首先,我们使用一个第三方类库jolcore,我使用的是0。10版本,帮助我们打印出对象头的数据。 我们可以通过下面这一段代码打印出Java的对象头:publicclassObjectHeaderPrinter{publicstaticvoidmain(String〔〕args)throwsInterruptedException{TesttestnewTest();System。out。println(打印匿名偏向锁对象头);System。out。println(ClassLayout。parseInstance(test)。toPrintable());synchronized(test){System。out。println(打印偏向锁对象头);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}}}复制代码 打印结果如下:打印匿名偏向锁无锁对象头Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)44(objectheader)00000000(00000000000000000000000000000000)(0)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal打印偏向锁对象头Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05a0804b(00000101101000001000000001001011)(1266720773)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal复制代码 我们把对象头的内存结构和对象头单独拿出来对照着解释一下: 128bit(对象头) 状态 64bitMarkWord 64bitKlassPoiter unused:25 identityhashcode:31 unused:1 age:4 biasedlock:1 lock:2 匿名偏向锁无锁 threadId:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 偏向锁 ptrtolockrecord:62 lock:2 轻量级锁 ptrtoheavyweightmonitor:62 lock:2 重量级锁 lock:2 GC标记 匿名偏向锁无锁我们给每个字节都标上序号。abcd05000000(00000101000000000000000000000000)(5)efgh00000000(00000000000000000000000000000000)(0)ijkl506a0600(01010000011010100000011000000000)(420432)复制代码 unused:25位,它实际上的字节应该是:hgfe的最高位。 identityhashcode:31位,它实际上的字节应该是:e的低7位dcb。 unused:1位,它实际上的字节应该是:a的最高位。 age:4位,它实际上的字节应该是:a的第47位 biasedlock:1位,它实际上的字节应该是:a的第3位 lock:2位,它实际上的字节应该是:a的低2位。 unused:25 identityhashcode:31 unused:1 age:4 biasedlock:1 lock:2 hgfe的最高位 e的低7位dcb a的最高位 a的第47位 a的第3位 a的低2位 0000000000000000000000000 0000000000000000000000000000000 0hr0000hr1hr01hr我们再来看一个加了偏向锁的对象头:偏向锁abcd05900013(00000101100100000000000000010011)(318803973)efgh01000000(00000001000000000000000000000000)(1)ijkl506a0600(01010000011010100000011000000000)(420432)复制代码 threadId:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 hgfedcb的高6位 b的低2位 a的最高位 a的第47位 a的第3位 a的低2位 000000000000000000000000000000010001001100000000100100 00hr0hr0000hr1hr01偏向锁 偏向锁是Java为了提高获取锁的效率和降低获取锁的代价,而进行的一个优化。因为Java团队发现大多数的锁都只被一个线程获取。基于这种情况,就可以认为锁都只被一个线程获取,那么就不会存在多个线程竞争的条件,因此就可以不需要真正的去获取一个完整的锁。只需要在对象头中写入获取锁的线程ID,用于表示该对象锁已经被该线程获取。 获取偏向锁,只要修改对象头的标记就可以表示线程已经获取了锁,大大降低了获取锁的代价。 当线程获取对象的偏向锁时,它的对象头: threadId:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 threadId:获取了偏向锁的线程ID epoch:用于保存偏向时间戳 age:对象GC年龄 biasedlock:偏向锁标记,此时为1 lock:锁标记,此时为10获取偏向锁 线程获取对象锁时,首先检查对象锁是否支持偏向锁,即检查biasedlock是否为1;如果为1,那么将会检查threadId是否为null,如果为null,将会通过CAS操作将自己的线程ID写入到对象头中。如果成功写入了线程ID,那么该线程就获取到了对象的偏向锁,可以继续执行后面的同步代码。 只有匿名偏向的对象才能进入偏向锁模式,即该对象还没有偏向任何一个线程(不是绝对的,存在批量重偏向的情况)。释放偏向锁 线程是不会主动释放偏向锁的。只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁。 释放偏向锁需要在全局安全点进行。释放的步骤如下:暂停拥有偏向锁的线程,判断是否处于同步代码块中,如果处于,则进行偏向撤销,并升级为轻量级锁。如果不处于,则恢复为无锁状态。 由此可以知道,偏向锁天然是可重入的。偏向撤销 偏向撤销主要发生在多个线程存在竞争,不再偏向于任何一个线程了。也就是说偏向撤销之后,将不会再使用偏向锁。具体操作就是将MarkWork中的biasedlock由1设置为0。偏向撤销需要到达全局安全点才可以撤销,因为它需要修改对象头,并从栈中获取数据。因此偏向撤销也会存在较大的资源消耗。 想要撤销偏向锁,还不能对持有偏向锁的线程有影响,所以就要等待持有偏向锁的线程到达一个safepoint安全点,在这个安全点会挂起获得偏向锁的线程。如果原持有偏向锁的线程依然还在同步代码块中,那么就会将偏向锁升级为轻量级锁。如果原持有偏向锁的线程已经死亡,或者已经退出了同步代码块,那么直接撤销偏向锁状态即可。 对象的偏向锁被撤销之后,对象在未来将不会偏向于任何一个线程。批量重偏向 我们可以想象,如果有100个对象都偏向于一个线程,此时如果有另外一个线程来获取这些对象的锁,那么这100个对象都会发生偏向撤销,而这100次偏向撤销都需要在全局安全点下进行,这样就会产生大量的性能消耗。 批量重偏向就是建立在撤销偏向会对性能产生较大影响情况下的一种优化措施。当JVM知道有大量对象的偏向锁撤销时,它就知道此时这些对象都不会偏向于原线程,所以会将对象重新偏向于新的线程,从而减少偏向撤销的次数。 当一个类的大量对象被同一个线程T1获取了偏向锁,也就是大量对象先偏向于该线程T1。T1同步结束后,另一个线程T2对这些同一类型的对象进行同步操作,就会让这些对象重新偏向于线程T2。在了解批量重偏向前,我们需要先了解一点其他知识: JVM会给对象的类对象class赋予两个属性,一个是偏向撤销计数器,一个是epoch值。 我们先来看一个例子:importorg。openjdk。jol。info。ClassLayout;importjava。util。ArrayList;importjava。util。List;authorliuhaidongdate20231615:06publicclassReBiasTest{publicstaticvoidmain(String〔〕args)throwsInterruptedException{延时产生可偏向对象默认4秒之后才能进入偏向模式,可以通过参数XX:BiasedLockingStartupDelay0设置Thread。sleep(5000);创造100个偏向线程t1的偏向锁ListTestlistAnewArrayList();Threadt1newThread((){for(inti0;i100;i){TestanewTest();synchronized(a){listA。add(a);}}try{为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活Thread。sleep(100000);}catch(InterruptedExceptione){e。printStackTrace();}});t1。start();睡眠3s钟保证线程t1创建对象完成Thread。sleep(3000);System。out。println(打印t1线程,list中第20个对象的对象头:);System。out。println((ClassLayout。parseInstance(listA。get(19))。toPrintable()));创建线程t2竞争线程t1中已经退出同步块的锁Threadt2newThread((){这里面只循环了30次!!!for(inti0;i30;i){TestalistA。get(i);synchronized(a){分别打印第19次和第20次偏向锁重偏向结果if(i18i19){System。out。println(第(i1)次偏向结果);System。out。println((ClassLayout。parseInstance(a)。toPrintable()));}if(i10){该对象已经是轻量级锁,无法降级,因此只能是轻量级锁System。out。println(第(i1)次偏向结果);System。out。println((ClassLayout。parseInstance(a)。toPrintable()));}}}try{Thread。sleep(10000);}catch(InterruptedExceptione){e。printStackTrace();}});t2。start();Thread。sleep(3000);System。out。println(打印list中第11个对象的对象头:);System。out。println((ClassLayout。parseInstance(listA。get(10))。toPrintable()));System。out。println(打印list中第26个对象的对象头:);System。out。println((ClassLayout。parseInstance(listA。get(25))。toPrintable()));System。out。println(打印list中第41个对象的对象头:);System。out。println((ClassLayout。parseInstance(listA。get(40))。toPrintable()));}}复制代码 在JDK8中,XX:BiasedLockingStartupDelay的默认值是4000;在JDK11中,XX:BiasedLockingStartupDelay的默认值是0t1执行完后,100个对象都会偏向于t1。t2执行完毕之后,其中前19个对象都会撤销偏向锁,此时类中的偏向撤销计数器为19。但当撤销到第20个的时候,偏向撤销计数器为20,此时达到XX:BiasedLockingBulkRebiasThreshold20的条件,于是将类中的epoch值1,并在此时找到所有处于同步代码块的对象,并将其epoch值等于类对象的epoch值。然后进行批量重偏向操作,从第20个对象开始,将会比较对象的epoch值是否等于类对象的epoch值,如果不等于,那么直接使用CAS替换掉MarkWord中的程ID为当前线程的ID。结论:前19个对象撤销了偏向锁,即MarkWord中的biasedlock为0,如果有线程来获取锁,那么先获取轻量级锁。第2030个对象,依然为偏向锁,偏向于线程t2。第31100个对象,依然为偏向锁,偏向于线程t1。 tech。youzan。comjavasuoyu 暂时无法在飞书文档外展示此内容批量撤销偏向 当偏向锁撤销的数量达到40时,就会发生批量撤销。但是,这是在一个时间范围内达到40才会发生,这个时间范围通过XX:BiasedLockingDecayTime设置,默认值为25秒。 也就是在发生批量偏向的25秒内,如果偏向锁撤销的数量达到了40,那么就会发生批量撤销,将该类下的所有对象都进行撤销偏向,包括后续创建的对象。如果在发生批量偏向的25秒内没有达到40,就会重置偏向锁撤销数量,将偏向锁撤销数量重置为20。Hashcode去哪了 我们通过MarkWord知道,在无锁状态下,如果调用对象的hashcode()方法,就会在MarkWord中记录对象的Hashcode值,在下一次调用hashcode()方法时,就可以直接通过MarkWord来得知,而不需要再次计算,以此来保证Hashcode的一致性。 但是获取了锁之后,就会修改MarkWord中的值,那么之前记录下来的Hashcode值去哪里了呢?LockRecord 在解答这个问题之前,我们需要先知道一个东西:LockRecord。 当字节码解释器执行monitorenter字节码轻度锁住一个对象时,就会在获取锁的线程栈上显式或者隐式分配一个LockRecord。换句话说,就是在获取轻量级锁时,会在线程栈上分配一个LockRecord。这个LockRecord说直白一点就是栈上的一块空间,主要用于存储相关信息。 LockRecord只要有三个作用:持有DisplacedWord(就是对象的MarkWord)和一些元信息用于识别哪个对象被锁住了。解释器使用LockRecord来检测非法的锁状态隐式地充当锁重入机制的计数器 那么这个LockRecord跟Hashcode有什么关系呢?场景1 我们先来看第一个场景:先获取对象的hashcode,然后再获取对象的锁。importorg。openjdk。jol。info。ClassLayout;publicclassTestObject{publicstaticvoidmain(String〔〕args){TesttestnewTest();步骤1System。out。println(获取hashcode之前);System。out。println(ClassLayout。parseInstance(test)。toPrintable());test。hashCode();步骤2System。out。println(获取hashcode之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());步骤3synchronized(test){System。out。println(获取锁之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}步骤4System。out。println(释放锁之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}}复制代码 运行结果:获取hashcode之前Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)44(objectheader)00000000(00000000000000000000000000000000)(0)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取hashcode之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)010c978b(00000001000011001001011110001011)(1953035263)44(objectheader)76000000(01110110000000000000000000000000)(118)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取锁之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)902a906b(10010000001010101001000001101011)(1804610192)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal释放锁之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)010c978b(00000001000011001001011110001011)(1953035263)44(objectheader)76000000(01110110000000000000000000000000)(118)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal复制代码步骤一:未获取对象的hashcode值之前,对象处于匿名偏向锁状态。锁标记为:101步骤二:获取对象的hashcode之后,对象的偏向状态被撤销,处于无锁状态。锁标记为:001。对象头中也存储了hashcode值,hashcode值为0111011100010111001011100001100。步骤三:获取锁之后,对象处于轻量级锁状态。锁标记为:00。其余62位为指向LockRecord的指针。从这里我们可以看到,MarkWord中已经没有hashcode了。整块MarkWord的内容已经被复制到LockWord中。步骤四:释放锁之后,对象处于无锁状态。锁标记为:001。在MarkWord中也可以看到之前生成的hashcode。与步骤二中的MarkWord一模一样。这是因为在释放锁之后,JVM会将LockRecord中的值复制回MarkWord中,并删除LockRecord。 结论:当对象生成hashcode之后,会撤销偏向,并将hashcode记录在MarkWord中。非偏向的对象获取锁时,会先在栈中生成一个LockRecord。并将对象的MarkWord复制到LockRecord中。场景2 我们现在来看第二个场景:先获取对象的锁,然后在同步代码块中生成hashcode。importorg。openjdk。jol。info。ClassLayout;publicclassHashCode2{publicstaticvoidmain(String〔〕args){TesttestnewTest();步骤一System。out。println(获取锁之前);System。out。println(ClassLayout。parseInstance(test)。toPrintable());synchronized(test){步骤二System。out。println(获取锁之后,获取hashcode之前);System。out。println(ClassLayout。parseInstance(test)。toPrintable());步骤三test。hashCode();System。out。println(获取锁之后,获取hashcode之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}步骤四System。out。println(释放锁之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}}复制代码 运行结果:获取锁之前Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)44(objectheader)00000000(00000000000000000000000000000000)(0)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取锁之后,获取hashcode之前Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)0590803a(00000101100100001000000000111010)(981504005)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取锁之后,获取hashcode之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)02e8832a(00000010111010001000001100101010)(713287682)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal释放锁之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)02e8832a(00000010111010001000001100101010)(713287682)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal复制代码步骤一:未获取对象的hashcode值之前,对象处于匿名偏向锁状态。锁标记为:101步骤二:进入同步代码块,线程获取了偏向锁。锁标记:101步骤三:对象生成hashcode,此时锁标记:10,直接从偏向锁升级为重量级锁。其余62位为指向objectMonitor的指针。 与轻量级锁存在同样的问题,hashcode会存放在哪里?每一个对象在JVM中都有一个objectMonitor对象,而MarkWord就存储在objectMonitor对象的header属性中。轻量级锁 轻量级锁解决的场景是:任意两个线程交替获取锁的情况。主要依靠CAS操作,相比较于使用重量级锁,可以减少锁资源的消耗。获取轻量级锁 使用轻量级锁的情况有以下几种:禁用偏向锁。偏向锁失效,升级为轻量级锁。 禁用偏向锁导致升级 在启动Java程序时,如果添加了JVM参数XX:UseBiasedLocking,那么在后续的运行中,就不再使用偏向锁。 偏向锁失效,升级为轻量级锁 如果对象发生偏向撤销时:首先会检查持有偏向锁的线程是否已经死亡,如果死亡,则直接升级为轻量级锁,否则,执行步骤2查看持有偏向锁的线程是否在同步代码块中,如果在,则将偏向锁升级为轻量级锁,否则,执行步骤3修改MarkWord为非偏向模式,设置为无锁状态。加锁过程 当线程获取轻量级锁时,首先会在线程栈中创建一个LockRecord的内存空间,然后拷贝MarkWord中的数据到LockRecord中。JVM中将有数据的LockRecord叫做DisplatedMarkWord。 LockRecord在栈中的内存结构: 暂时无法在飞书文档外展示此内容 当数据复制成功之后,JVM将会使用CAS尝试修改MarkWord中的数据为指向线程栈中DisplatedMarkWord的指针,并将LockRecord中的owner指针指向MarkWord。 如果这两步操作都更新成功了,那么则表示该线程获得轻量级锁成功,设置MarkWord中的lock字段为00,表示当前对象为轻量级锁状态。同步,线程可以执行同步代码块。 如果更新操作失败了,那么JVM将会检查MarkWord是否指向当前线程的栈帧:如果是,则表示当前线程已经获取了轻量级锁,会在栈帧中添加一个新的LockRecord,这个新LockRecord中的DisplatedMarkWord为null,owner指向对象。这样的目的是为了统计重入的锁数量,因此,在栈中会有一个LockRecord的列表。完成这一步之后就可以直接执行同步代码块。 暂时无法在飞书文档外展示此内容如果不是,那么表示轻量级锁发生竞争,后续将会膨胀为重量级锁。释放轻量级锁 释放轻量级锁时,会在栈中由低到高,获取LockRecord。查询到LockRecord中的DisplatedMarkWord为null时,则表示,该锁是重入的,只需要将owner设置为null即可,表示已经释放了这个锁。如果DisplatedMarkWord不为null,则需要通过CAS将DisplatedMarkWord拷贝至对象头的MarkWord中,然后将owner的指针设置为null,最后修改MarkWord的lock字段为01无锁状态。重量级锁 重量级锁解锁的场景是:多个线程相互竞争同一个锁。主要通过park()和unpark()方法,结合队列来完成。相较于轻量级锁和偏向锁,需要切换内核态和用户态环境,因此获取锁的过程会消耗较多的资源。获取重量级锁 使用重量级锁的情况有两种:在持有偏向锁的情况下,直接获取对象的hashcode,将会直接升级为重量级锁。在轻量级锁的情况下,存在竞争,膨胀为重量级锁。 获取hashcode,升级为重量级锁importorg。openjdk。jol。info。ClassLayout;publicclassHashCode2{publicstaticvoidmain(String〔〕args){TesttestnewTest();步骤一System。out。println(获取锁之前);System。out。println(ClassLayout。parseInstance(test)。toPrintable());synchronized(test){步骤二System。out。println(获取锁之后,获取hashcode之前);System。out。println(ClassLayout。parseInstance(test)。toPrintable());步骤三test。hashCode();System。out。println(获取锁之后,获取hashcode之后);System。out。println(ClassLayout。parseInstance(test)。toPrintable());}}}复制代码 执行后的结果获取锁之前Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)44(objectheader)00000000(00000000000000000000000000000000)(0)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取锁之后,获取hashcode之前Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)0590803a(00000101100100001000000000111010)(981504005)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal获取锁之后,获取hashcode之后Testobjectinternals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)02e8832a(00000010111010001000001100101010)(713287682)44(objectheader)01000000(00000001000000000000000000000000)(1)84(objectheader)506a0600(01010000011010100000011000000000)(420432)124intTest。i0Instancesize:16bytesSpacelosses:0bytesinternal0bytesexternal0bytestotal复制代码 我们直接在偏向锁的同步代码块中执行hashcode(),会发现偏向锁直接膨胀为重量级锁了。我们可以看到lock字段为10。 这里有一个疑问,为什么不是升级为轻量级锁呢?轻量级锁也可以在LockRecord中存储生成的hashcode。而膨胀为更为消耗资源的重量级锁。 轻量级锁膨胀为重量级锁 当处于轻量级锁的时候,说明锁已经不再偏向于任何一个线程,但是也没有发生竞争,可以依靠CAS获取到轻量级锁。但是当出现CAS获取锁失败时,就会直接膨胀为重量级锁。 这里需要注意,只会CAS一次,只要一次失败就会直接膨胀为重量级锁,而不是达到自旋次数或者自旋时间才膨胀。膨胀过程 在膨胀过程中,会有几种标记来表示锁的状态:Inflated:膨胀已完成Stacklocked:轻量级锁INFLATING:膨胀中Neutral:无锁 膨胀步骤:检查是否已经为重量级锁,如果是直接返回。检查是否处于膨胀中的状态,如果是,循环检测状态。检测出膨胀中的状态是因为有其他线程正在进行膨胀,因为需要等待膨胀完成之后,才能继续执行。检查是否为轻量级锁,如果是,则执行以下步骤:创建一个ObjectMonitor对象。通过CAS设置MarkWord为全0,用以表示INFLATING状态。如果失败,则从步骤1重新开始执行。将MarkWord设置到ObjectMonitor对象中。设置owner属性为LockRecord设置MarkWord值返回判定为无锁状态,执行以下步骤:创建一个ObjectMonitor对象。通过CAS直接设置MarkWord值。返回竞争锁过程 我们要理解如何获取重量级锁,需要先了解ObjectMonitor对象。顾名思义,这是一个对象监视器。在Java中,每个对象都有一个与之对应的ObjectMonitor。ObjectMonitor内部有几个重要的字段:cxq:存放被阻塞的线程EntryList:存放被阻塞的线程,在释放锁时使用WaitSet:获得锁的线程,如果调用wait()方法,那么线程会被存放在此处,这是一个双向循环链表onwer:持有锁的线程 cxq,EntryList均为ObjectWaiter类型的单链表。 获取锁过程通过CAS设置onwer为当前线程(尝试获取锁),CAS的原值为NULL,新值为currentthread,如果成功,则表示获得锁。否则执行步骤2判断当前线程与获取锁线程是否一致,如果一致,则表示获得锁(锁重入)。否则执行步骤3判断当前线程是否为之前持有轻量级锁的线程,如果是,直接设置onwer为当前线程,表示获得锁。否则执行步骤4以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤5 使用阻塞和唤醒来控制线程竞争锁通过CAS设置owner为当前线程(尝试获取锁),CAS的原值为NULL,新值为currentthread。如果成功,则表示获得锁。否则执行步骤b通过CAS设置owner为当前线程(尝试获取锁)CAS的原值为DEFLATERMARKER,新值为currentthread。如果成功,则表示获得锁。否则执行步骤c。(DEFLATERMARKER是一个锁降级的标记,后续会讲解。)以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤d。为当前线程创建一个ObjectWaiter类型的node节点。步骤i和ii是一个循环,直到一个成功才会跳出这个循环。通过cas插入cxq的头部,如果插入失败,则执行步骤ii通过CAS设置owner为当前线程(尝试获取锁),CAS的原值为NULL,新值为currentthread。如果失败,则执行i。通过CAS设置owner为当前线程(尝试获取锁),CAS的原值为NULL,新值为currentthread。如果成功,则表示获得锁。否则执行步骤f。(该步骤往下开始是一个循环,直到获取到锁为止)通过park(),将线程阻塞。线程被唤醒后通过CAS设置owner为当前线程(尝试获取锁),CAS的原值为NULL,新值为currentthread。如果成功,则表示获得锁。否则执行步骤ii通过CAS设置owner为当前线程(尝试获取锁)CAS的原值为DEFLATERMARKER,新值为currentthread。如果成功,则表示获得锁。否则执行iii尝试一轮自旋来获取锁。如果未获取锁,则跳转回步骤e执行。 自适应自旋锁主要是用于重量级锁中,降低阻塞线程概率。而不是用于轻量级锁,这里大家要多多注意。释放重量级锁释放锁过程判断owner字段是否等于currentthread。如果等于则判断当前线程是否为持有轻量级锁的线程,如果是的话,表示该线程还没有执行enter()方法,因此,直接设置owner字段为currentthread。判断recursions,如果大于0,则表示锁重入,直接返回即可,不需要执行后续解锁代码。设置owner字段为NULL,解锁成功,后续线程可以正常获取到锁。唤醒其他正在被阻塞的线程。在执行以下操作之前需要使用该线程重新获取锁。如果获取锁失败,则表示锁已经被其他线程获取,直接返回,不再唤醒其他线程。(为什么还要获取到锁才可以唤醒其他线程呢?因为唤醒线程时,需要将cxq中的节点转移到EntryList中,涉及到链表的移动,如果多线程执行,将会出错。)如何EntryList非空,那么取EntryList中的第一个元素,将该元素下的线程唤醒。否则执行步骤b。将cxq设置为空,并将cxq的元素按照原顺序放入EntryList中。然后取EntryList中的第一个元素,将该元素下的线程唤醒。线程唤醒设置owner字段为NULL,解锁成功,让后续线程可以正常获取到锁。然后调用unpark()方法,唤醒线程。wait(),notify(),notifyAll() 我们需要知道一个前提,在处理wait方法时,必须使用重量级锁。因此,wait方法会导致锁升级。 我们先来看一个例子:publicclassWaitTest{staticfinalObjectlocknewObject();publicstaticvoidmain(String〔〕args){newThread((){synchronized(lock){log(getlock);try{log(waitlock);lock。wait();}catch(InterruptedExceptione){thrownewRuntimeException(e);}log(getlockagain);log(releaselock);}},threadA)。start();sleep(1000);newThread((){synchronized(lock){log(getlock);createThread(threadC);sleep(2000);log(startnotify);lock。notify();log(releaselock);}},threadB)。start();}publicstaticvoidcreateThread(StringthreadName){newThread((){synchronized(lock){log(getlock);log(releaselock);}},threadName)。start();}privatestaticvoidsleep(longsleepVal){try{Thread。sleep(sleepVal);}catch(Exceptione){e。printStackTrace();}}privatestaticvoidlog(Stringdesc){System。out。println(Thread。currentThread()。getName():desc);}}复制代码 最后打印的结果:threadA:getlockthreadA:waitlockthreadB:getlockthreadB:startnotifythreadB:releaselockthreadA:getlockagainthreadA:releaselockthreadC:getlockthreadC:releaselock复制代码线程A首先获取到锁,然后通过wait()方法,将锁释放,并且等待通知。睡眠1S,这里是确保线程A可以顺利完成所有操作。因为A释放了锁,所以线程B可以获取到锁。然后创建了线程C。因为线程B睡眠了2S,依然持有锁,所以线程C无法获取到锁,只能继续等待。线程B调用notify()方法,线程A被唤醒,开始竞争锁。线程A和线程C竞争锁。 但是根据打印结果,无论执行多少次,都是线程A先获取锁。 第一个问题:为什么都是线程A先获取锁,而不是线程C先获取锁? 第二个问题:为什么wait方法并没有生成monitorenter指令,也可以获取到锁? 第三个问题:执行wait之后,线程去哪里了?它的状态是什么? 为了解答这些问题,我们需要深入到源码中去。但是这里就不放源码了,我只讲一下关键步骤: wait()膨胀为重量级锁为currentthread创建ObjectWaiter类型的node节点将node放入waitSet中释放锁通过park()阻塞currentthread。 notify()检查waitSet是否为null,如果为null,直接返回获取waitSet的第一个元素node,并将其从链表中移除。此时,存在三个策略:默认使用policy2插入到EntryList的头部(policy1)插入到EntryList的尾部(policy0)插入到cxq的头部(policy2)将node插入到cxq的头部。 notifyAll()循环检测waitSet是否不为空如果不为空,则执行notify()的步骤。否则返回 第一个问题:执行wait之后,线程去哪里了?它的状态是什么? 线程A调用wait()方法后,线程A就被park了,并被放入到waitSet中。此时他的状态就是WAITING。如果它从waitSet移除,并被放入到cxq之后,那么他的状态就会变为BLOCKED。如果它竞争到锁,那么他的状态就会变为RUNNABLE。 第二个问题:为什么wait方法并没有生成monitorenter指令,也可以获取到锁? 线程A调用wait()方法后,线程A被放入到waitSet中。直到有其他线程调用notify()之后,线程A从waitSet移除,并放入到cxq中。 第三个问题:为什么都是线程A先获取锁,而不是线程C先获取锁? 线程A调用wait()方法后,线程A被放入到waitSet中。线程B获取锁,然后创建了线程C,线程C竞争锁失败,被放入到cxq中。然后B调用notify()方法后,线程A从waitSet移除,放入到cxq的头部。因此目前cxq的链表结构为:ACnull。接着线程B释放锁,会将cxq中的元素按照原顺序放入到EntryList中,因此目前cxq链表结构为:null;EntryList链表结构为:ACnull。然后唤醒EntryList中的第一个线程。 所以,每次都是线程A先获取锁。 来源:https:juejin。cnpost7194053886250860600