synchronized锁膨胀过程验证

Posted 一键破光阴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了synchronized锁膨胀过程验证相关的知识,希望对你有一定的参考价值。

synchronized的小故事

在并发编程中,对于synchronizedLock的使用是很频繁的。Lock是基于Java的一个框架,synchronized是基于C语言的JVM层级的锁,使用起来很方便。

但是呢,在jdk1.6以前,synchronized是一个重量级锁,他的底层需要操作系统来支持,必然涉及到了用户态和内核态的切换,还需要挂起当前线程,直到竞争到锁才执行,因此是很耗性能的。再者,一个C语言程序性能竟然不如Java的Lock框架性能,也是让人哭笑不得。

所以啊,Oracle公司决定对synchronized进行优化,在jdk1.6对该锁进行升级,增加了锁膨胀升级、锁粗化、锁消除等特性,使得两种锁的性能达到了差不多的水平。

本文重点介绍锁的膨胀升级,并使用代码验证其过程。


锁其实是一个对象

锁其实就是一个Object对象,对于新手来说,可能不好理解。这里简单解释一下,方便理解后面的概念。

synchronized是基于JVM内置锁——Monitor(监视器)实现。任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态,即可以执行同步块里的代码。synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

简单理解就是任何一个对象都可以成为一把锁,类的Class也可以成为一把锁,这把锁锁定的级别就是类级别,可以对静态方法进行锁定。

既然,锁是对象,那么回顾一下对象的布局:对象头、实例数据、对齐填充。锁相关的信息就保存在对象头里边。

HotSpot虚拟机的对象头包括两部分信息:Mark Word、类型信息,如果是数组需要多加一个数组长度。

Mark Word,用于存储对象自身的运行时数据, 如哈希码
(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。

这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

这里的Mark Word空间复用怎么理解呢?就是一块空间,会根据锁当前的状态(锁会升级,下文会介绍)进行调整,具体布局如下

假设当前是无锁状态,那么Mark Word保存的就是对象的hashcode、对象分代年龄、是否偏向、锁标志位
如果锁是轻量级锁,那么此时Mark Word保存的就是pinter to Lock Record,是一个指针,指针所指向的位置,会拷贝以根当前的Mark Word信息(分代年龄、hashcode…)


锁怎么膨胀升级

对于可能会有线程安全问题的代码,我们一般会对其加锁,使得程序能串行执行。但是很多时候,我们系统的并发度是很低的,甚至几乎没有出现过并发情况,这样加锁就有点浪费性能了(每次都让操作系统去加锁、释放锁)。

故此,可以多加几种锁,让锁能够适配不同场景,同时也能够根据合适的场景自动升级:偏向锁、轻量级锁、重量锁。这些锁是可以进行单向升级的,也就是说不能降级。

偏向锁

顾名思义,锁会偏向某一个线程。如果在一定时间内,只有一条线程去获取锁,那么就没有必要去中断线程、竞争锁。
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,
无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

默认是开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的。倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

有些文章、博客会提到自旋锁,其实就是轻量级锁的一种实现:不中断线程,通过不断循环获取锁,当然这个循环是有次数限制的。

重量级锁

该锁就是需要由操作系统进行加锁、释放锁,性能开销较大,但是在激烈的锁竞争环境下,重量级锁性能会比多条线程不断自旋要来的好。


锁膨胀升级验证

首先引入一个能够动态打印对象头信息的依赖

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
        </dependency>
public class MarkWordTest 

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException 

        Thread t1 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t1:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t2 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t2:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t3 = new Thread(() -> 
                synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t3:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t4 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t4:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        System.out.println("start:" + ClassLayout.parseInstance(lock).toPrintable());
        // t1开始
        t1.start();

        TimeUnit.SECONDS.sleep(5);
        // t2、t3开始
        t2.start();
        t3.start();

        TimeUnit.SECONDS.sleep(5);
        System.out.println("t2、t3end:" + ClassLayout.parseInstance(lock).toPrintable());
    



由图可见,这段程序锁状态变化过程是:无锁---->轻量级锁---->重量级锁---->无锁

有心的人应该会注意到一个问题,偏向锁状态去哪儿了呢?默认不是开启的么?

我在代码中把锁对象实例化进行延迟,待虚拟机都启动后,我再进行实例化,可以得到如下结果:

由图可见,这段程序锁状态变化过程是:可偏向---->偏向锁---->重量级锁---->无锁

这是什么原因导致的呢?

这个问题,我也没有深入研究过,大家可以看看其他博文,结论就是虚拟机把一开始就创建的对象锁状态直接跳过偏向的状态。


锁不能降级

对于锁不能降级的概念,我之前一直以为的是:锁状态升到了重量级锁,之后再有线程竞争锁时候就直接用重量级锁实际上并非如此。这里结合代码一起看看吧。

public class MarkWordTest 
    
    private static Object lock;

    public static void main(String[] args) throws InterruptedException 

        Thread t1 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t1:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t2 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t2:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t3 = new Thread(() -> 
                synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t3:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );

        Thread t4 = new Thread(() -> 
            synchronized (lock) 
                try 
                    TimeUnit.MILLISECONDS.sleep(2);
                    System.out.println("t4:" + ClassLayout.parseInstance(lock).toPrintable());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );


        TimeUnit.SECONDS.sleep(5);
        lock = new Object();
        System.out.println("start:" + ClassLayout.parseInstance(lock).toPrintable());
        // t1开始  ---->偏向锁
        t1.start();

        TimeUnit.SECONDS.sleep(5);
        // t2、t3开始   ---->重量级锁
        t2.start();
        t3.start();

        TimeUnit.SECONDS.sleep(5);
        System.out.println("t2、t3end:" + ClassLayout.parseInstance(lock).toPrintable());
        // t4开始  ---->轻量级锁
        t4.start();
    



这段代码大致意思如下:
一开始有一条线程来获取锁,由于只有当前一条线程竞争,所以就进行CAS,将当前线程id设置到锁对象Mark Word上面,锁状态变更为偏向锁。
接着两条线程一起进来,由于竞争比较激烈,直接升级成重量级锁。
前两条线程都结束,此时锁状态是无锁
最后一条线程进来竞争锁,虽然此时只有一条线程进来,但是锁状态还是变成轻量级锁。

我一开始会觉得,锁状态怎么能从重量级锁降级成轻量级锁???这么神奇?

这里应该是自己理解不到位。这里锁膨胀升级是针对一轮锁竞争的过程,竞争结束后,自然又会从轻量级开始。可能有人会问为什么不是从偏向锁开始?我也实验了好几次,确实只会偏向一次,后边不会再去偏向另一个线程,都是从轻量级锁开始。可能虚拟机觉得没有必要吧,都出现过轻量级锁、重量级锁的状态,何必从最低级的开始呢?

java锁膨胀过程

        今天看了一个视频教程,其中讲到了锁膨胀的过程,这个课程通过一个java main很容易的让人理解synchronized的优化以及锁膨胀的过程。在上代码之前先上张对象头markword的图片如下:

代码如下(主线程再加上新起的两个线程,三个线程同步锁stepNum导致锁升级,由偏向锁到轻量级锁,到重量级锁):

import org.openjdk.jol.info.ClassLayout;

public class LockEvolutionShow {
    public static void main(String[] args) {
        Integer initNum = new Integer("1");// 初始化一个对象,默认是无锁状态(状态为non-biasable)最后三位是001,注意打印的结果是16进制,转为二进制即可
        System.out.println("【non-biasable】===对象头MarkWord末尾三位【001】=" + ClassLayout.parseInstance(initNum).toPrintable());

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Integer stepNum = new Integer("2");// JVM默认启动后等待3s后就会把对象标记为偏向锁(状态为biasable),此时打印结果可以看到后三位是101。,注意打印的结果是16进制,转为二进制即可
        System.out.println("【biasable】===对象头MarkWord末尾三位【101】=" + ClassLayout.parseInstance(stepNum).toPrintable());

        // 添加同步原语后,可以看到对象被标记为偏向锁,注意打印的后三位是101(状态为biased),注意打印的结果是16进制,转为二进制即可
        synchronized (stepNum) {
            System.out.println(
                    "【biased lock】===对象头MarkWord末尾三位【101】=" + ClassLayout.parseInstance(stepNum).toPrintable());
        }
        System.out.println(// 退出同步块后,可以看到对象依然被标记为偏向锁(状态为biased),并未清除,此时打印结果可以看到后三位是101。,注意打印的结果是16进制,转为二进制即可
                "【biased lock release]===对象头MarkWord末尾三位【101】=" + ClassLayout.parseInstance(stepNum).toPrintable());

        new Thread(() -> {// 启动了一个线程,线程的并发特性(上面的主线程和这个新起的线程并发,同步锁stepNum),导致锁升级,这时候升级为轻量级锁(状态为thin
                          // lock),此时打印结果可以看到后两位是00。,注意打印的结果是16进制,转为二进制即可
            synchronized (stepNum) {
                System.out.println(
                        "【thin lock】===对象头MarkWord末尾两位【00】=" + ClassLayout.parseInstance(stepNum).toPrintable());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 注意这个线程保持锁至少5s,导致下面又新起的线程同步锁stepNum,再次锁升级,由轻量级锁thin变为重量级fat,此时打印结果后两位是10。
                System.out.println("【thin lock -> fat lock】===对象头MarkWord末尾两位【10】="
                        + ClassLayout.parseInstance(stepNum).toPrintable());
            }
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            synchronized (stepNum) {
                ////注意这个线程同步锁stepNum,导致锁升级,由轻量级锁thin变为重量级fat,此时打印结果后两位是10。
                System.out.println(
                        "【fat lock】===对象头MarkWord末尾两位【10】=" + ClassLayout.parseInstance(stepNum).toPrintable());
            }
        }).start();
    }
}

【non-biasable】===对象头MarkWord末尾三位【001】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【biasable】===对象头MarkWord末尾三位【101】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【biased lock】===对象头MarkWord末尾三位【101】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000000025ba005 (biased: 0x00000000000096e8; epoch: 0; age: 0)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【biased lock release]===对象头MarkWord末尾三位【101】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000000025ba005 (biased: 0x00000000000096e8; epoch: 0; age: 0)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【thin lock】===对象头MarkWord末尾两位【00】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001f08f6f0 (thin lock: 0x000000001f08f6f0)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【thin lock -> fat lock】===对象头MarkWord末尾两位【10】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001c0f7b4a (fat lock: 0x000000001c0f7b4a)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

【fat lock】===对象头MarkWord末尾两位【10】=java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001c0f7b4a (fat lock: 0x000000001c0f7b4a)
8 4 (object header: class) 0xf80022ad
12 4 int Integer.value 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 

PS:头部markword 8个字节,初始状态【fat lock】的value是16进制0x000000001c0f7b4a ,转为二进制是11100000011110111101101001010,末尾是10对照上面的markword图片可知是重量级锁。

 

以上是关于synchronized锁膨胀过程验证的主要内容,如果未能解决你的问题,请参考以下文章

java锁膨胀过程

多线程高并发之Synchronized锁及其膨胀

JDK源码Synchronized关键字原理,和锁的膨胀过程

Java 有什么锁

synchronized 锁的膨胀过程(锁的升级过程)深入剖析

Synchronized和锁升级