Java并发编程之对象内存布局与锁升级过程

    技术2025-09-19  23

    对象内存布局-JOL(Java Object Layout)

    package pro.eddie.demo; import org.openjdk.jol.info.ClassLayout; public class JavaObjLayout { public static void main(String[] args) { Demo demo = new Demo(); System.out.println(ClassLayout.parseInstance(demo).toPrintable()); } private static class Demo { private int i = 666; private int j = 777; } }

    运行结果:

    可以发现对象布局分为4部分:

    MarkWord:记录了该对象的状态,有:无锁状态,加锁状态(偏向锁、自旋锁、重量锁),GC标记状态。class类型指针:通过该指针快速定位对应的Class类,getClass()方法则是通过该指针进行访问实例数据:存放对象中的属性,若是基本数据类型则VALUE为对应的值,若是对象,则存放对象引用对齐:64位操作系统下,对齐部分将会自动将对象内存补齐至能够被8整除的大小,既易于管理便于寻址,也避免产生碎片

    偏向锁及其应用场景

    偏向锁的实现是:Unlock状态下MarkWord的一个比特位用于标识该对象偏向锁是否被使用或者是否被禁止。如果该bit位为0,则该对象未被锁定,并且禁止偏向;如果该bit位为1,则意味着该对象已经在偏向锁开启状态,默认对象都是可偏向**匿名偏向(Anonymously biased)的,这是有一个线程来使用这个对象后,则可能转变为可重偏向(Rebiasable)或已偏向(Biased)状态;这时有其他线程再来访问该对象,通过判断持有此对象的线程,是否是正在使用此对象,若没有,则该对象是可重偏向(Rebiasable)**状态,通过CAS原子操作,来该对象的偏向锁绑定于线程自身。

    适用场景:适用于单线程操作,线程数一多,偏向锁容易升级成轻量级锁,此时撤销偏向锁也需要耗费资源。

    轻量级锁/自旋锁及其应用场景

    轻量级锁/自旋锁即为CAS

    适用场景:少量线程并发,且每个线程执行时间较短;原因:CAS操作依然占用着CPU的资源(取值、赋值、比较),若线程数量一多,线程执行时间一长,导致线程的自旋次数大大增多,得不偿失。

    重量级锁及其应用场景

    实现:每个Java对象与一个monitor绑定关联,当一个线程想要执行一个Synchronized代码块内的内容时,得先拿到对应对象的monitor。并有以下规则:

    若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)

    若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数

    若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权

    能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程,执行monitorexit时会将monitor的进入数减1。

    当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

    HotSpot实现的Monitor是由Cpp编写的ObjectMonitor:

    ObjectMonitor() { _header = NULL; _count = 0; //monitor进入数 _waiters = 0, _recursions = 0; //线程的重入次数 _object = NULL; _owner = NULL; //标识拥有该monitor的线程 _WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁进入时的单项链表 FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接),cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程),因此_cxq是一个后进先出的stack(栈)。_EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中_WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中

    适用场景:多线程并发,线程执行时间较长的情况下,在对象被锁定时,其他并发的线程处于阻塞状态,不会占用CPU资源。

    锁升级的过程(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)

    无锁 -> 偏向锁:

    默认情况下:main线程执行后,4秒延迟后才会开启偏向锁机制。

    原因:JVM虚拟机自己有一些默认启动的线程,有不少的sync代码,这些代码启动时,就存在锁竞争,如果使用偏向锁,就会有许多锁撤销,锁升级的操作,使得效率降低。

    偏向锁 -> 轻量级锁:

    程序启动4秒后,默认启动了偏向锁机制,当一个线程首次去访问MarkWord标记为偏向锁的对象(匿名偏向),则修改MarkWord指向该线程;第二个线程访问该对象时,发现该对象偏向锁被持有,则通过MarkWord上的线程信息去访问该线程,并判断该线程是否仍然需要持有偏向锁:不需要继续持有对象则通过CAS操作撤销该偏向锁,通过CAS操作修改MarkWord指向自身;需要继续持有对象的情况则通过CAS操作修改MarkWord锁类型信息,升级为轻量级锁。

    轻量级锁 -> 重量级锁:

    自旋锁(轻量级锁)在JDK1.4.2引入,使用-XX:+UseSpinning来开启。

    自旋超过10次,升级为重量级锁。

    JDK6中变成默认开启,并引入了自适应的自旋锁(适应性自旋锁)。

    适应性自旋锁:意味着自旋时间(次数)不再固定,而是由前一次在同一个锁上的自选时间及锁的拥有者状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。(简单来说就是:根据该锁上的成功的概率来决定是否要升级锁)

    Processed: 0.029, SQL: 9