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

6 篇文章 0 订阅

对象内存布局-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;
    }
}

运行结果:
jol



可以发现对象布局分为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资源。



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

  1. 无锁 -> 偏向锁:

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

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

  2. 偏向锁 -> 轻量级锁:

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

  3. 轻量级锁 -> 重量级锁:

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

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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值