背景:
Java语言相较于c,c++的一个优势,是其从语言层面就提供了对多线程同步以及相互协作的支持;而不像c,c++,要通过其他的库才能写多线程程序。在Java语言里,通过synchronize关键字来同步一段关键代码片断(critical area),而通过wait,notify来实现线程间相互协作。本文主要描述一下sun的hotspot是如何来支持synchronize及wait和notify的。
在hotspot的实现里,每个java对象有一个对应的c++对象,每个用来表达java对象的c++对象都有一个header,占用2个word,第一个word成为mark word,第二个word则用来指向一个klass,该klass用来表征该对象的类型信息。第一个mark word,在某些情况下会用来表示同步信息,所以需要我们重点探究一下。
1.Mark Word
mark word是用来表征对象一些信息的对象头,目前依据jvm是32位还是64位,而分为32位64位2种形式。由于2者差别不大,所以我们这里以32位为例子,来讲解一下mark word。
在32位jvm上,markword为32位长,根据具体各个bit的数值的不同,可以组合成4种大类(表明该头markword所在的对象当前的状态),具体如下图所示。

我们现在需要关注的是前2类,即当markword表示该对象一个普通java对象或者被biased locking的对象。
2.hotspot当前对synchronize的实现类型及其应用场景
当前的hotspot共有3种类型的锁,来实现synchronize的语义,之所以有3种,是因为这3种要解决的问题不同,所做的优化也不同。这3种锁分别为biased locking,stack lock,infalted(ObjectMonitor).简单除暴的来讲,从轻量级上来说,biased lock最优,inflated 最差。
为了便于大家的认知,先从优化做的最少的Infalte锁开始讲起。
2.1 Inflate锁的实现以及其应用场景
当该锁被不同的线程争用或者有线程调用该对象那个的wait方法时,会使用Infalte锁。Inflate的实现方式是将该object的markword指向一个ObjectMonitor对象,然后利用该ObjectMonitor来实现同步以及wait,notify功能。
下面来简单介绍一些ObjectMonitor的主要字段及方法,其主要字段有:
1._owner,用来表示当前持有该monitor的线程,如果没有没有被线程持有,则为null;本质上来讲,一个线程要想持有该monitor,就必须在该monitor的_owner字段为null时,使用原子操作来将该字段替换成自己。
2._cxq,一个ObjectWaiter类型的链表,ObjectWaiter代表了一个等待该锁的线程对象。在hotspot里,把最近的需要获取该锁而block住的线程都放入到_cxq里。
3._EntryList,也是一个ObjectWaiter类型的链表,像较于_cxq,其区别是_EntryList里存放的线程可能等待的时间都比较久了。之所以要分成2个链表,一来是这样可以使得_cxq的元素数量比较适中,这样当当前线程要释放该锁时,如果采取首先从_cxq中去寻找下一个线程来获取该锁,时间会缩短。当一个线程释放锁时,根据策略,可能会将_cxq和_EntryList合并成一个链表。
4._WaitSet,用来存放调用该锁的wait()方法而还未被notify的线程。
5.Responsible,Thread类型,由于hotspot在锁释放时时,做了一些优化(没有做锁释放后,调用membar指令),很可能导致释放锁的线程已经将该锁释放,而想要竞争该锁的线程由于看到的是陈旧的数据(认为锁还是被释放线程持有),从而出现Stranding现象。因此,为了使Stranding现象尽可能的短,需要从众多竞争线程中指定一个Responsible线程,该线程在竞争锁失败后,不是调用永久的park,而是定期醒过来,检查当前的monitor是否已经可以空闲了。这个Responsible字段就是用来存放该线程的。
ObjectMonitor的主要方法的流程:
1.enter方法大致流程(线程要争用该锁时,调用此方法):1.如果该monitor还空闲,通过CAS来获取该锁。2.处理递归获取锁的问题(当前线程已经持有该锁了)。3.试图通过spin方式来获取monitor。4.调用EnterI方法来获取锁。
2.EnterI方法大致流程(争用锁过程中的一个内部方法):1.企图使用tryLock方式来获取monitor。2.企图使用spin方式来获取monitor。3.将自己放入到_cxq队列进行等待。4.进入一个无限循环,在该循环里,不断的通过tryLock方式来获取锁,一旦获取到锁,退出该循环。5.调用UnlinkAfterAcquire方法,将自己从_cxq和EntryList中剔除(因为此时已经获得锁了)。6.如果发现Responsible的值是自己,将Responsible字段赋值为null.
3.wait方法大致流程(当线程调用该锁的wait方法时,会调用此方法):1.将自己包装成WaiterObject,并且放入到_WaitSet链表里去,然后调用exit方法释放对锁的控制;2.如果此时发现线程被唤醒,则调用unpark方法,让线程开始运行。3.否则调用park方法,让本线程block。4,当线程从park方法返回时,说明有其他线程调用了该线程的unpark方法,此时线程将自己从_WaitSet中脱离出来,并对自己的状态进行处理,如果经过处理后,线程的状态为run,则调用enter方法,以便重新获取锁。5.如果线程状态不是run,则会进入retenter处理流程。6.当线程获得锁后,会坚持之前的过程是否有interrupt发生,如果有,抛出Interrupted异常。
4.exit方法大致流程(当线程要释放该锁时):1.处理递归退出情况(即该线程已经获取该monitor多次),2.判断继承人是否自己已经准备好,如果已经准备好了,则本线程直接退出,否则,进入步骤4,挑出一个继承者,并调用unpark激活其运行。否则,调用park方法来讲当前线程进行阻塞。
上面描述的是这些方法的大致流程,这一块的复杂性还在于为了确保对变量的可见性,需要在很多地方调用内存同步原语,比如OrderAccess::fence(),OrderAccess::storeload()等,非常的细节和专业。
总结:所以Inflated(ObjectMonitor)锁,由于其内部有各种字段,能够处理一般的锁争用,也能够处理wait,notify等情况。当要block一个锁时,其本质上调用和操作系统实现相关的park方法来阻塞当前线程,相当于是和系统函数有交互,成本较大同时,为了处理争用情况,调用了大量的内存同步原语,这些同步原语相对而言,会导致比较长时间的latency(相对于普通的计算机指令周期而言),而且如果使用Inflated锁,则需要为每个锁分配一个对应的ObjectMonitor对象,对内存也有一些损耗。
2.2Stack lock的实现以及其应用场景
当一个锁只被一个线程使用(不存在不同线程的争用情况),从而也没有wait,notify操作时,只要使用stack lock即可。stack lock是在持有该lock的线程堆栈上分配的,其自身有一个类型为markOop的_displaced_header字段,可以用来存放该锁所在对象的markword。
stack lock时线程获取锁的过程:
1.判断该锁所在的对象的markword信息是否表明该锁还没有被持有,如果是这样,则先将此markword信息存放进stack lock(此stack lock由jit从此线程的frame空间中分配,并且一个线程的所有lock都会被集中管理,便于处理递归获取与递归释放的情形)的_displaced_header字段,然后通过原子替换,将对象头的markword值替换为stack lock,如果成功,说明此线程第一次获取到该锁,退出。2.如果第一步没有成功,则判断该锁是否属于这个线程的堆栈空间,如果是这种情况,则表明该线程已经持有了该锁,这次行为是想再一次持有该锁,此时只需要将该lock的_displaced_header设置为NULL。表明此lock代表的是该线程对该锁的一次递归持有(非第一次持有)。
stack lock时线程释放锁的过程:
1.判断此时的stack lock的_displaced_header是否为null,如果是,则表明是一个代表递归锁的lock,do nothing,直接返回;2.如果_displaced_header不为null,则表明是最后一次释放该锁,通过原子替换,将_displaced_header里面存放的原始markword值拷贝会该锁所在的对象头。3.如果上述2种情况都不是,或者原子替换不成功,则说明此锁发生了多线程间的争用情况,系统对其进行inflate,将其变为inflate锁。
一些还未经证实的推测(这部分内容是通过在openjdk 邮件组上发问题,别人回复给我的,还没有查看相关代码,所以还未经证实):如果使用stack lock,则线程会在其frames里,持有一个stack lock chain,最外面的stack lock代表该线程第一次获取该锁(_displaced_header的值为markword),而后续的stack lock,则表示递归持有,其_displaced_header皆为null;当要释放一个锁时,会取出最近的stack lock,如果发现它的_displaced_header为null,则直接忽略,以此类推,只有当真正拿到最后一个stack lock时,其_displaced_header不会null,此时,该线程真正释放了该锁,需要将_displaced_header指向的markwrod放回到该锁所在对象的头部,从而进行复原,表明此时该锁已经没有线程持有了。
总结:stack lock锁,适用于同一个线程持有一个锁(可以多次持有),当其第一次持有和释放时,需要进行原子操作(递归持有和释放时,则不需要进行原子操作,代价小很多)。
2.3Biased lock的实现以及其应用场景
所谓biased lock,只译过来就是偏向锁,则是针对如果该锁在成个程序运行期间只被一个线程持有的情况做的优化。对于stack lock,毕竟当第一次获取锁和最终释放锁时,会产生CAS操作,而CAS操作在SMP机器上,由于需要同步cache和主存上的内容,会造成较大的延时(大概是几百个cpu指令周期),hotspot的开发人员觉得其性能太慢了( 这可能是我们长期写上层应用程序的人没法理解的),而biased lock的初衷就是为了减少CAS操作。同时,sun的研究人员也指出,现在的cpu设计人员,已经将很多精力focus在提高这类原子操作的性能上了,这也逐渐造成了biased lock收益越来越小的这样一个现实。
biased lock时线程释获取锁的过程:
1.线程通过判断该锁所在对象的头,确定其是否biasable & unbiased,如果是这种情况,则通过一个CAS操作,对该对象的头部进行操作,是得该对象头部的前25位为此Thread的地址。2.如果该锁已经被持有,则判断是否是本线程持有,如果是,则do nothing,直接获取锁。3.如果前2种情况都不是,则说明该锁有抢占,则会等待safepoint的情况,然后对持有该锁的线程进行操作,将其替换成stack lock形式(这里没有直接替换成infalte 形式的锁,sun的相关人员回答是因为如果在这里替换成inflate锁,代码会很复杂,不符合关注点分离原则)。
biased lock时线程释放锁的过程:
1.biased lock被释放时,不会对对象头做任何改变。直到有其他线程需要争用这个锁时,该锁才会被revoke或者rebiase,从而改变对象头。隐含的含义:当一个对象的对象头是biased,并且其markword的前25bit指向了一个线程时,系统并不能通过这些来真正断定该25bit指向的线程就拥有了该biased lock,系统还需要review该线程的frame,查看在frame里面的lock record相关的信息(有点类似于stack lock chain),要结合这些lock record,才能真正判定该线程是否真正持有该biased lock。
hotspot在biased lock上的一些实现特点:1.由于markword的复用性,所以biased lock和hashcode其实使用了同一个markword,这样的后果是如果一个lock时使用biased方式来表示的,而当我们对该对象调用了hashcode方法时,系统会计算其hashcode,而hashcode的值会覆盖biased的内容,从而造成biased lock被revoke成stack lock。这一点我个人感觉是一个很大弊端,因为在现实的应用中,计算一个object的hashcode是非常普遍的操作,而如果因此造成biased lock被revoke的话,这样其实对性能影响挺大的。2.sun还根据epoch来确定是否对某一个type的所有对象都进行revoke,而epoch的值则根据运行时的各个对象的biased lock的revoke情况统计计算出来的。
biased lock和stack lock使用场景的异同:
1.前面已经说过了,stack lock的使用场景是没有竞争的情景;那么biased lock的使用场景则是既没有竞争,又没有共享的情况(即该lock在整个生命周期里,只为一个线程持有)。
2.在同一个线程里,当执行如下代码时:
public void synchronized foo(){
doSomeThing();
}
public void synchronized bar(){
doSomeThing();
}
public void test(){
for(int i = 0; i < 10; i++){
foo();
bar();
}
}
如果只有一个线程执行这个test方法时,如果使用biased locking,则只会产生一次CAS,第一次获取锁时,需要通过原子操作来将Thread的地址设置到markword里(退出test方法时,markword的值仍不改变,只有当其他线程来获取该锁时,才会被改变);如果只使用普通的stack lock(没有使用coarseing lock技术时),则需要进行40次的CAS。
最后,贴上一张图(引用至http://wikis.sun.com/display/HotSpotInternals/Synchronization),来表明在各种状态下,markword里的内容:

1.当后3位为101时,表明是biasable lock。
2.当后3位为001时,表明是regular object。
3.当后2位为00时,表明是stack lock,前面的bit用来指向在线程堆栈中的lock record。
4.当后2位为10时,表明是object monitor,前面的bit用来指向object monitor的地址。
一些链接:
biased locking(from Dave)Dave应该是sun里主要负责线程同步这一块的研究人员,人很热心,我在学习过程中碰到问题,发了封邮件给他,他给我回了封很长的邮件,详细进行了解答
HotSpotInternals_Synchronization
推荐的pdf:
Eliminating synchronization-related atomic operations with biased locking and bulk rebiasing.pdf