专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

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

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

运行结果:

90_1.png

可以发现对象布局分为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中变成默认开启,并引入了自适应的自旋锁(适应性自旋锁)。

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

文章永久链接:https://tech.souyunku.com/45761

未经允许不得转载:搜云库技术团队 » Java并发编程之对象内存布局与锁

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们