synchronized关键字(锁升级案例)

java对象

Java实例对象对包括三部分:对象头、对象体和对齐字节,对应如下表

普通对象

markword8Bytes -> 64bit用于标记锁信息、GC信息、IdentityHashCode等
Class Pointer 类指针4Bytes -> 32bit用于标记该对象是哪个Class的实例开启内存压缩(-XX:+UseCompressedClassPointer)后为4字节,不开启内存压缩为8个字节
成员变量视成员变量的类型和数量而定如果没有成员变量,则这一块为空
Padding 对齐视上述字节而定一个对象占用的字节数必须是8的倍数,不足的用padding对齐

数组对象布局

markword8Bytes用于标记锁信息、GC信息、IdentityHashCode等
Class Pointer类指针4Bytes用于标记该对象是哪个Class的实例开启内存压缩(-XX:+UseCompressedClassPointer)后为4字节,不开启内存压缩为8个字节
数组长度4Bytes标记数组有多少个元素
数组内容根据数组类型m和长度n而定,长度为m*n如果元素为基本类型,比如byte/boolean/short/char/int/long/double,则m为对应的长度;如果元素为数组,m是4字节的引用如果数组长度为0,这一块为空
Padding 对齐视上述字节而定一个对象占用的字节数必须是8的倍数,不足的用padding对齐

Mark Word

在32位JVM虚拟机中,Mark Word和Class Pointer这两部分都是32位的;
在64位JVM虚拟机中,Mark Word和ClassPointer这两部分都是64位的。
在堆内存小于32GB的情况下,64位虚拟机的UseCompressedOops选项是默认开启的,该选项表示开启Oop对象的指针压缩会将原来64位的Oop对象指针压缩为32位。(-XX:+UseCompressedOops)

markword
markword
其中markword前三个位代表锁状态

锁状态
在这里插入图片描述
Mark Word属性:

  • lock:锁状态标记位,占两个二进制位
  • biased_lock:是否启用偏向锁标记,只占一个二进制位。为1时表示启用偏向锁,为0时表示没有偏向锁
  • age:4位的Java对象分代年龄(0000-1111,-XX:MaxTenuringThreshold最大值为15)
  • identity_hashcode:31位的对象标识HashCode采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中
  • thread:54位的线程ID值为持有偏向锁的线程ID
  • epoch:偏向时间戳
  • ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针
  • ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下指向对象监视器的指针

其中idea中打印对象头信息可以使用如下工具

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

如何使用

public static void main(String[] args) {
    Object o = new Object();
    String s = ClassLayout.parseInstance(o).toPrintable();
    System.out.println(s);
}

打印台输出
在这里插入图片描述

接下来我们来介绍synchronized的锁和升级过程

无锁状态(无锁不可偏向)

当我们新创建出一个对象并没有执行任何操作的时候,该对象往往是无锁可偏向状态(-XX:BiasedLockingStartupDelay=0该状态下),如果没有上述红色指令,案例如下

无锁案例

代码

控制台输出及分析
在这里插入图片描述
此处可以看出当前对象是无锁不可偏向的,此时如果调用synchronized则会变成轻量锁,原因是因为jvm内部默认偏向延迟4秒

偏向锁状态

偏向锁案例

//jvm参数
-XX:BiasedLockingStartupDelay=0

java代码

public static void main(String[] args) {
    Object o = new Object();
    String s = ClassLayout.parseInstance(o).toPrintable();
    System.out.println(s);
}

控制台输出及分析
偏向锁
此处为偏向锁,但是还未有线程id记录,只是该对象可偏向,该状态可以改为无锁状态,当我们调用object的hashcode方法,可以将可偏向状态改为无锁

可偏向->无锁

public static void main(String[] args) {
    Object o = new Object();
    String s = ClassLayout.parseInstance(o).toPrintable();
    // 计算hashcode之前
    System.out.println("计算hashcode之前:"+s);
    int i = o.hashCode();
    s = ClassLayout.parseInstance(o).toPrintable();
    // 计算hashcode之后
    System.out.println("计算hashcode之后:"+s);
}

控制台输出及分析
在这里插入图片描述
由于红色框内记录了对象的hashcode,所以当无锁状态升到轻量锁的时候无法存储方法栈中的锁记录指针,所以偏向锁变为了无锁

偏向锁记录线程id案例

public static void main(String[] args) {
    Object o = new Object();
    synchronized (o) {
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }
}

控制台输出及分析
在这里插入图片描述
由于没有计算hashcode,使jvm有存储空间来存储线程的id,表明该对象已被该线程偏向了

轻量锁

偏向锁计算了hashcode,或者两个线程交替持有锁,将升级到轻量锁

偏向锁->轻量锁案例
代码:

public static void main(String[] args) {
    Object o = new Object();
    synchronized (o) {
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }
    new Thread(() -> {
        synchronized (o) {
            String s1 = ClassLayout.parseInstance(o).toPrintable();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(s1);
        }
    }).start();
}

控制台输出及分析
偏向锁升级到轻量锁
由于主线程和我们创建出来的线程交替获取o的锁,所以此时偏向锁升级到了轻量锁

重量锁

当两个线程发生了锁竞争执行,并超出轻量锁spin次数之后,轻量级锁将升级成为重量锁
重量锁案例

public static void main(String[] args) {
    Object o = new Object();
    String s = ClassLayout.parseInstance(o).toPrintable();
    System.out.println("初始状态:"+s);
    new Thread(() -> {
        synchronized (o) {
            String s1 = ClassLayout.parseInstance(o).toPrintable();
            System.out.println("第一个线程:"+s1);
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(() -> {
        synchronized (o) {
            String s1 = ClassLayout.parseInstance(o).toPrintable();
            System.out.println("主线程阻塞两秒后,第二个线程:"+s1);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();


}

控制台输出及分析
在这里插入图片描述
在这里我们可以看到锁直接从偏向锁"直接"膨胀到了重量锁,其实并不是这样,偏向锁先升级到了轻量锁,由于迟迟获取不到锁,所以升级到了重量锁。具体可以看案例的代码

总结

  1. 无锁:Java对象刚创建时还没有任何线程来竞争,该对象处于无锁状态,这时偏向锁标识位是0,锁状态是01
  2. 偏向性:偏向锁状态的Mark Word会记录获取锁的线程ID,下次获取锁只需要对比线程ID即可,在竞争不激烈的情况下效率非常高
  3. 轻量级锁:当锁处于偏向锁,另一个线程进行抢占时,偏向锁就会升级为轻量级锁。进行抢占的线程会通过自旋的形式尝试获取锁,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要进行内核态和用户态之间的切换来进入阻塞挂起状态,但是,线程自旋是需要消耗CPU的,如果一直获取不到锁,那么线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。JVM对于自旋周期的选择,JDK 1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的。线程如果自旋成功了,下次自旋的次数就会更多,如果自旋失败了,自旋的次数就会减少。
  4. 重量级锁:重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。

锁升级流程:https://blog.csdn.net/qicha3705/article/details/120459815

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值