上一节你了解了什么是CAS、synchronizd形成的锁的类型、重量级锁是用户态进程向内核态申请资源加锁过程,HotSpotJava对象结构,以及初步从3个层面分析了下synchronizd的核心流程。还记得核心流程图么?
如下所示:
文章配图这一节我们仔细来分析下这个过程中,每一步的底层原理。我们需要用到一个工具包,JOL,它可以将java对象的信息打印出来。你可以通过这个工具分析升级过程中锁的标记变化。
synchronizd锁升级流程详解
synchronizd锁升级流程详解
首先是我们看一下:
偏向锁未启动:无锁态nw-普通对象。
偏向锁已启动:无锁态nw-匿名偏向锁。
我们来看个例子:设置JVM参数,-XX:BiasdLockingStartupDlay=10环境:JDK1.8
dpndncygroupIdorg.opnjdk.jol/groupIdartifactIdjol-co/artifactIdvrsion0.10/vrsion/dpndncy
publicclassHlloSynchronizd{publicstaticvoidmain(String[]args){Objctobjct=nwObjct();Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());synchronizd(objct){}}}
输出结果如下:
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)()(1)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
大端还是小端序?Systm.out.println(BytOrdr.nativOrdr());可以查看当前cpu的字节序。输出是LITTLE_ENDIAN意味着是小端序l小端序:数据的高位字节存放在地址的高端低位字节存放在地址低端l大端序:数据的高位字节存放在地址的低端低位字节存放在地址高端比如一个整形0x,1是高位数据,7是低位数据。按照小端序01放在内存地址的高位,比如放在0x,23就放在0x以此类推。大端序反之。
如下图:(图片来源于网络)
可以看到OFFSET为0-4的Objcthadr的Valu中这个标记。
也就是说,Objcto=nwObjct()默认的锁=表示了无锁态注意:如果偏向锁打开,默认是匿名偏向状态。
可以修改JVM参数-XX:BiasdLockingStartupDlay=0。再次运行
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)(0000)(5)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
可以看到OFFSET为0-4的Objcthadr的Valu中这个标记。表示一个偏向锁,为什么说是匿名的呢?因为在JVM底层C++代码中,偏向锁默认有一个C++变量JavaThad指针,使用54位记录这个指针,从OFFSET为0-4的Objcthadr的Valu中看到除了锁的标记为是外,其余都是0,表示没有JavaThad指针无,所以是一个匿名偏向。
偏向锁未启动是指什么?偏向锁未启动指默认情况偏向锁有个时延,默认是4秒(不同JDK版本可以不一样)可以通过一个JVM参数控制,-XX:BiasdLockingStartupDlay=4。因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
所以这个2个流程的变化如下图所示:
文章配图接着我们看往后看:
偏向锁已启动:无锁态nw-匿名偏向锁-》偏向锁偏向锁未启动:无锁态nw-普通对象-》偏向锁
当执行到同步代码时候,有了明确的加锁线程,所以我们增加一行日志,打印Objct的对象头信息,会发现,已经发生如下变化:
publicclassHlloSynchronizd{publicstaticvoidmain(String[]args){Objctobjct=nwObjct();Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());synchronizd(objct){Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());}}}
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)(0000)(5)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotaljava.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)05f8ba86(00)(-)44(objcthadr)b0000(1)()84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
可以看到OFFSET为0-4的Objcthadr的Valu中这个标记之外,不在全部是0,说明已经不再是匿名偏向锁了。
如果原来不是匿名偏向锁,只是一个普通对象,进入synchronizd代码块后,会直接变成偏向锁。如下图所示:
文章配图偏向锁未启动:无锁态nw-普通对象-轻量级锁(自旋锁)
接下来我们看一下,无锁也有可能直接变成轻量级锁。设置JVM参数,-XX:BiasdLockingStartupDlay=10,在synchronizd内部加入JOL的打印输出,就会打印如下对象信息:
//-XX:BiasdLockingStartupDlay=10publicstaticvoidmain(String[]args){Objctobjct=nwObjct();Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//nw-普通对象synchronizd(objct){Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//nw-轻量锁00}}
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)()(1)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotaljava.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)0f09f70(11111)()44(objcthadr)2a()(42)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
此流程如下图所示:
文章配图偏向锁-轻量级锁(轻度竞争)
当有线程竞争锁时,会撤销偏向锁,升级轻量级锁。
//-XX:BiasdLockingStartupDlay=0publicstaticvoidmain(String[]args){Objctobjct=nwObjct();Systm.out.println("初始化nw");Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//+全是0匿名偏向锁synchronizd(objct){Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//+非0偏向锁}nwThad(()-{try{Thad.slp(0);synchronizd(objct){Systm.out.println("t线程获取锁");Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//00objct被另一个线程加锁,发生竞争,偏向锁-轻量锁}}catch(IntrruptdExcption){}}).start();}
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)(0000)(5)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotaljava.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)cc74(010)()44(objcthadr)b3000()()84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)d8f3df89()(-)44(objcthadr)46(10)(70)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
可以看到锁的变化从匿名偏向-偏向-轻量锁。这里简单提下轻量锁的底层原理:
当变成轻量锁,如果有别的线程尝试获取锁,会在线程在自己的线程栈生成LockRcordC++对象,用CAS操作将markword中62位地址,使用引用(C++叫指针)指向自己这个线程的对应的LR对象,如果设置成功者得到锁,否则继续CAS执行循环自旋操作。(PS:轻量锁的底层是使用一个LockRcordC++对象,偏向使用的是JavaThad这个对象指针)
整个升级流程如下图所示:
文章配图偏向锁-重量级锁(重度竞争)
很早之前JDK判断竞争加剧的条件是:有线程超过10次自旋(可以通过-XX:PBlockSpin)或者自旋线程数超过CPU核数的一半。但是1.6之后,加入自适应自旋AdapativSlfSpinning的机制,由JVM自己控制升级重量级锁。
升级时,向操作系统申请资源,通过linuxmutx申请互斥锁,CPU从3级到0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
//-XX:BiasdLockingStartupDlay=0publicstaticvoidmain(String[]args){Systm.out.println(BytOrdr.nativOrdr());Objctobjct=nwObjct();Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//+全是0匿名偏向锁Systm.out.println("初始化nw");synchronizd(objct){Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//+非0偏向锁}for(inti=0;i10;i++){nwThad(()-{try{Thad.slp(0);synchronizd(objct){Systm.out.println(Thad.curntThad().gtNam()+"线程获取锁");Systm.out.println(ClassLayout.parsInstanc(objct).toPrintabl());//10objct被多个线程竞争,偏向锁-重量锁}}catch(IntrruptdExcption){}}).start();}}
java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)(0000)(5)44(objcthadr)()(0)84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal//初始化nwjava.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)(000)(-)44(objcthadr)1f(011)()84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal//Thad-0线程获取锁java.lang.Objctobjctintrnals:OFFSETSIZETYPEDESCRIPTIONVALUE04(objcthadr)f57ff()(-)44(objcthadr)1f(011)()84(objcthadr)()()(lossdutothnxtobjctalignmnt)Instancsiz:16bytsSpaclosss:0bytsintrnal+4bytsxtrnal=4bytstotal
上面的代码可以看出,锁升级是从匿名偏向锁-偏向锁-重量锁的过程,JVM判断出for循环中创建了10个线程,竞争激烈,当线程获取锁的时候直接就是重量级锁。如下图所示:
文章配图最后一条线,轻量级锁到重量级锁的代码我就不演示了,当竞争加剧的时候,轻量级锁会升级为重量级锁的。
好了,到这里相信你对synchronizd的锁升级流程已经理解的非常清楚了。接下来我们看一些锁升级过程中的一些原理和细节。
锁升级流程中的核心原理和细节
锁升级流程中的核心原理和细节
既然synchronizd的锁机制和java对象头的结构密切相关,对象头中的markword有锁标记,分代年龄,指针引用等含义。接下来就让我们仔细分析下偏向锁、自旋锁、重量级锁它们的底层原理和对象头中的markword的联系。
偏向锁的基本原理
文章配图轻量锁的C++实现机制和可重入性(基于栈)
轻量锁的原理和偏向锁类似,只不过markWord中的指针是一个LockRcord,并且修改指针的操作为CAS,那个线程CAS设置成功就会获取锁。如下图所示:
文章配图synchronizd的锁是可重入的,这样子类才可以调用父类的同步方法,不会出问题。使用同一个对象或者类也可以多次加synchronizd的代码块。所以轻量锁重入性的实现是基于入栈LR对象,来记录重入次数的。如下所示:
文章配图重量锁的C++实现机制和可重入性(基于ObjctMonitor类似于AQS)
重量级锁的底层原理,是通过在MarkWord里就有一个指针,是指向了这个对象实例关联的monitor对象的地址,这个monitor是c++实现的,不是java实现的。这个monitor实际上是c++实现的一个ObjctMonitor对象,里面包含了一个_ownr指针,指向了持有锁的线程。ObjctMonitor它的C++结构体如下:
//objctMonitor.hppObjctMonitor(){_hadr=NULL;_count=0;//重入次数_waitrs=0,_cursions=0;_objct=NULL;_ownr=NULL;//获得锁的线程_WaitSt=NULL;//调用wait()方法被阻塞的线程_WaitStLock=0;_Rsponsibl=NULL_succ=NULL;_cxq=NULL;FNxt=NULL;_EntryList=NULL;//ContntionList中那些有资格成为候选人的线程被移到EntryList_SpinFq=0;_SpinClock=0;OwnrIsThad=0;_pvious_ownr_tid=0;}
ObjctMonitor里还有一个ntrylist,想要加锁的线程全部先进入这个ntrylist等待获取机会尝试加锁,实际有机会加锁的线程,就会设置_ownr指针指向自己,然后对_count计数器累加1次。
各个线程尝试竞争进行加锁,此时竞争加锁是在JDK1.6以后优化成了基于CAS来进行加锁,理解为跟之前的LockAPI的加锁机制是类似的,CAS操作,操作_count计数器,比如说将_count值尝试从0变为1。
如果成功了,那么加锁成功了count加1,修改成;如果失败了,那么加锁失败了,就会进入waitSt等待。
然后释放锁的时候,先是对_count计数器递减1,如果为0了就会设置_ownr为null,不再指向自己,代表自己彻底释放锁。
如果获取锁的线程执行wait,就会将计数器递减,同时_ownr设置为null,然后自己进入waitst中等待唤醒,别人获取了锁执行类似notifyAll的时候就会唤醒waitst中的线程竞争尝试获取锁。
整个过程如下所示:
文章配图可能你会问,那尝试加锁这个过程,也就是对_count计数器累加操作,是怎么执行的?如何保证多线程并发的原子性呢?
很简单,这个地方count操作是一个类似于CAS的操作。
其实,你如果了解RntrantLock底层的AQS机制,你就会发现,synchronizd底层的实现和AQS差不多的。
只不过synchronizd的底层是ObjctMonitor,它的地位就跟RntrantLock里的AQS对应的实现Sync组件是差不多的。之后我们讲到RntrantLock的时候你就会发现了。
为什么有自旋锁还需要重量级锁?
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。
重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。
偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销vok,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁(自旋锁)。
比如JVM启动过程,会有很多线程竞争(已经明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。
锁消除
publicvoidadd(Stringstr1,Stringstr2){StringBuffrsb=nwStringBuffr();sb.appnd(str1).appnd(str2);}
我们都知道StringBuffr是线程安全的,因为它的关键方法都是被synchronizd修饰过的,但我们看上面这段代码,我们会发现,sb这个引用只会在add方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此sb是不可能共享的资源,JVM会自动消除StringBuffr对象内部的锁。
锁粗化
publicStringtst(Stringstr){inti=0;StringBuffrsb=nwStringBuffr():whil(i){sb.appnd(str);i++;}turnsb.toString():}
JVM会检测到这样一连串的操作都对同一个对象加锁(whil循环内次执行appnd,没有锁粗化的就要进行次加锁/解锁),此时JVM就会将加锁的范围粗化到这一连串的操作的外部(比如whil虚幻体外),使得这一连串操作只需要加一次锁即可。
wait和notify必须和sychronizd一起使用!?
wait和notify必须和sychronizd一起使用!?
wait和notify/notifyAll还是挺有用的,在多线程开发中和很多开源项目中。那么如何使用wait和notifyall呢?它们的作用主要是线程通信,所以某个线程可以用wait处于等待状态,其他线程可以用notify来通知它,或者说是唤醒它。
wait与notify实现的一个底层原理其实和synchronizd的重量级锁原理类似,主要也是monitor对象。需要注意的是必须得对同一个对象实例进行加锁,这样的话,他们其实操作的才是通一个对象实例里的monitor相关的计数器、waitst。
换句话说,wait与notify,必须在synchronizd代码块中使用。因为wait/notify底层都是C++代码,是针对ObjctMonitor进行操作的。
举个例子:
publicstaticvoidmain(String[]args)throwsIntrruptdExcption{Objcto=nwObjct();ThadwaitThad=nwThad(()-{try{synchronizd(o){Systm.out.println(Thad.curntThad().gtNam()+"线程获取锁,进行wait操作");o.wait();Systm.out.println(Thad.curntThad().gtNam()+"线程继续执行,之后释放了锁");}}catch(IntrruptdExcption){}});waitThad.start();ThadnotifyThad=nwThad(()-{try{Thad.slp();synchronizd(o){Systm.out.println(Thad.curntThad().gtNam()+"线程获取锁,执行notify唤醒操作");o.notify();Systm.out.println(Thad.curntThad().gtNam()+"线程继续执行,之后释放了锁");}}catch(IntrruptdExcption){}});notifyThad.start();}
上面代码的流程如下图所示:
文章配图上面过程涉及很多细节,需要仔细研究HotSpotC++代码,有兴趣的同学可以研究下wait和notify/notifyAll的C++代码。
大多情况下,核心还是掌握ObjctMonitor这个实现机制原理即可。你可能还有一些疑问,我找了一些wait和notify相关的常见的问题,供大家参考。
(以下转载自: