Java互斥语义的实现

对象头(Object Header)

HotSpot 虚拟机的对象头包括两部分信息:Mark Word(标记字段)和 Klass Pointer(类型指针)

 

  1. Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。JVM 对象头一般占用两个机器码,在 32-bit JVM 上占用 64bit, 在 64-bit JVM 上占用 128bit 16 bytes(暂不考虑开启压缩指针的场景)。另外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

    存储内容

    标志位

    状态

    对象哈希码、对象分代年龄

    01

    未锁定

    指向锁记录的指针

    00

    轻量级锁定

    指向重量级锁的指针

    10

    膨胀(重量级锁定)

    空,不需要记录信息

    11

    GC标记

    偏向线程ID、偏向时间戳、对象分代年龄

    01

    可偏向

    2.Klass Pointer,即是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

     

    偏向锁

    Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了"偏向锁""轻量级锁",在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

    HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    1)偏向锁的撤销

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。图2-1中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

    2)关闭偏向锁

        偏向锁在Java 6Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-

    UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

     

    轻量级锁 BasicObjectLock

    1)轻量级锁加锁

    线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

    2)轻量级锁解锁

    轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是两个线程同时争夺锁,导致锁膨胀的流程图。

     

    把要加锁对象Mark Word拷贝到栈帧Lock Record中,并把对象Mark Word执行Lock Record。

    普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。

    如果对象没有被锁定

    将对象头的Mark指针保存到锁对象中

    将对象头设置为指向锁的指针(在线程栈空间中)

    1. lock->set_displaced_header(mark);
    2. if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    3. TEVENT (slow_enter: release stacklock) ;
    4. return ;
    5. }

    如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)

    在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗

    在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降。

     

    3.锁的优缺点对比

     

    自旋锁

    Jdk1.6以上的自旋锁根据虚拟机监控信息自适应选择自旋时间长度和是否自旋。当竞争存在时,线程可以先等待一下,这时锁持有线程可能已经释放锁,从而避免互斥同步挂起和恢复线程的消耗。这个等待是CPU执行一些操作,可能自旋消耗大于线程挂起恢复。

    注:内置于JVM中的获取锁的优化方法和获取锁的步骤

    偏向锁可用会先尝试偏向锁

    轻量级锁可用会先尝试轻量级锁

    以上都失败,尝试自旋锁

    再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

     

    [1]    本节一些内容参考了HotSpot源码、对象头源码markOop.hpp、偏向锁源码

    biasedLocking.cpp,以及其他源码ObjectMonitor.cppBasicLock.cpp

     

    锁优化

    减小锁粒度

    将大对象,拆成小对象,大大增加并行度,降低锁竞争

    偏向锁,轻量级锁成功率提高,例如:ConcurrentHashMap

    锁分离

    读写分离思想可以延伸,只要操作互不影响,锁就可以分离

    LinkedBlockingQueue

    锁粗化

    一个操作中有个多个加锁片段或者加多个锁。避免多次加锁开销可以把多个锁合并。

    锁消除

    在即时编译器时,如果发现所有数据不可能被共享,则可以对锁消除。

     

posted @ 2017-07-08 14:10  jiumao  阅读(895)  评论(0编辑  收藏  举报