Synchronized锁机制与膨胀过程

概述

这篇文章主要介绍了JVM中Synchronized锁实现的机制。
主要分为几个部分:

  • 虚拟机对Synchronized的处理以及锁机制
  • 虚拟机对Synchronized锁的优化
  • Synchronized锁的膨胀过程图解
  • 查看对象头在Synchronized的上锁,释放锁,以及膨胀过程中的变化

虚拟机对Synchronized的处理

了解虚拟机类文件结构的同学们一定知道,对于synchronzied方法块而言,虚拟机在块内的方法前后会增加moniterentermoniterexit两个指令,而对于synchronized方法来说,在方法的ACCESS_FLAG中会出现一个ACC_SYNCHRONIZED的标志位,虚拟机会根据该标识位隐式的执行同步过程。

这两种都是由管程(Monitor)支持实现的(应该说是在虚拟机未对锁优化前)。一个线程在上锁的时候会尝试获取对象关联的monitor,如果该monitor未被其他线程获取,那么该线程将会获得此monitor,将ownership改为自己,并将锁的计数器加1。否则线程将进入monitor的等待队列,等待monitor被释放后再尝试获取。
整个过程是基于mutex互斥量来实现的,因此需要涉及用户态和内核态的切换,会消耗很多处理器时间。因此,基于该方式实现的synchronized锁也被称为重量级锁。

虚拟机对Synchronized锁的优化

JDK1.6之后对传统的Synchronized的锁做了很多优化,尽量避免重量级锁的直接使用,提高线程在上锁和释放锁时的效率。

重量锁(互斥锁)

上文已经介绍了传统的synchronzied锁是基于mutex互斥量的,其主要的缺点是是在上锁过程中可能需要挂起线程,涉及用户态和内核态的切换,浪费处理器时间。

轻量级锁:

轻量级锁的轻量级是相对于基于mutex互斥量实现的重量级锁而言。
在我们大部分的程序中,线程间的竞争并不激烈,且线程并不会长时间的持有锁。如果在不存在竞争并且锁将立被释放的情况下,也通过重量级锁去上锁和释放锁,那么对锁的操作浪费的时间可能比代码执行的时间更多。
轻量级锁通过CAS设置加自选等待的方式解决了上述这种场景下重量级锁低效的问题。
在使用轻量级锁时,线程会尝试通过CAS更新锁对象的对象头,如果更新成功,说明成功标记对象。如果更新失败,则说明该对象已经被其他线程持有,线程会进入自选等待,因为通常一个线程不会长时间的持有锁,因此很可能尝试获取锁的线程只需要几次自旋获取锁。
如果一段时间自选后,线程依旧无法获取锁,那么轻量级锁才会被升级成为重量级锁。

偏向锁

虽然轻量级锁已经极大的提升了锁的效率,但是线程每次上锁和释放锁依然会产生时间的浪费。而一种极端的情况下,一个锁可能都是由某个线程去获取的(也就是其他线程不太会去获取这个锁,也就是不存在竞争的情况)。
偏向锁就是出于对上述这种情况而进行的优化,希望将无竞争下的同步过程消除。
偏向锁会偏向第一个获取他的线程,之后就算该线程退出同步方法,偏向锁对该线程的标记依旧在,这样做的好处是该线程之后获取锁和释放锁都不需要进行CAS更新操作。只需要对比偏向锁的标记是否未自己。
直到有其他线程获取该锁时,发现该锁标记的对象不是自己,则会要求该锁升级。

编译器对锁的优化

除了上述锁实现机制的优化外,编译器还通过自旋,锁消除,锁粗化的方式对锁进行优化。

自旋

自旋在介绍轻量级锁时也介绍到了,当线程发现锁被持有时,线程不会立即挂起,而是尝试自选等待。
这样做的好处是,避免了操作系统在用户态和内核态的来回切换。但是缺点是自旋等待会白白占用处理器的运行时间。

锁消除

锁消除是指在一些不存在竞争的情况下,编译器会取消掉同步的过程。

锁粗化

锁粗化是指某一线程在一个方法内频繁的上锁和释放锁,编译器会主动扩大一次上锁覆盖的范围,减少上锁和释放锁的次数。

锁的膨胀过程图解

上文介绍了虚拟机对Synchronized锁做了优化。
在开启了偏向锁的情况下,先会使用偏向锁,当有线程竞争偏向锁时,会发生锁的升级,偏向锁会升级为轻量级锁。
如果轻量级锁超过一定自旋次数,仍旧无法获取,那么会发生锁膨胀,变成重量级锁,通过mutex的方式实现互斥。
并且这一过程中会造成对象头中Mark Word的改变,或者说对象头中的Mark Word会记录着这一过程的变化。
那么什么是Mark Word?OpenJDK中给出的定义如下:

mark word:The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

Mark Word:是每个对象头中第一个字。用一组位表示同步锁状态和哈希值等,也可能指向同步锁相关的信息(如遇字符,则用小端)。在GC阶段,还包含了GC状态。

从这里我们基本可以了解到 MarkWord 是一个标志对象诸多状态的一字长的数据(32位虚拟机和64位虚拟机所占位数不同)。

更具体的,我们可以通过下图(原图出处)了解下对象头中Mark Word在各种锁状态下的结构(这里以64位虚拟机为例,32位虚拟机基本类似)。
image

处于节约内存的目的考虑,MarkWord的一字长的数据会在不同状态下用来表示不同的信息。
从右往左看,最后两位是锁状态的标志位。结合锁标志位和偏向锁标识位,我们就可以区分当前对象锁的状态,其余的位可能会用来记录线程或是其他相关的指针信息。

大致了解完 Mark Word结构后,我们通过另一张图片(原图出处)了解锁膨胀的过程,结合过程中Mark Word的改变。关于Mark Word的详细说明将在下文介绍。
image
这幅图非常详细,我们可以拆成三部分逐个过程分析:

偏向锁的上锁与释放过程,以及锁升级

image

轻量级锁的上锁与释放过程,以及锁升级

image

重量级锁的上锁与释放

这部分流程比较简单,不再赘述。

对象头查看

上面从理论上介绍了锁的升级过程,但是对于对象头这种看不见摸不着的信息,可能依然有同学看的懵里懵懂。
好在openjdk提供了一个利器帮助我们打印对象头信息——jol-core库。
可以通过 maven添加到我们的库中:

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

在开始测试前,有一点需要明确:32位虚拟机和64位虚拟机对象头的结构是有一些差异的(本文测试均是基于64位虚拟机),且结构是以小端的方式存储数据。

测试准备:

以下是我们测试的一些基础类:;Monitor类作为对象锁的类,主要是验证MarkWord关于HashCode的内容:

Foo类

保存上锁的方法

public class Foo {

    private Monitor lock = new Monitor();


    public void sync(){
        synchronized (lock){
            System.out.println("------------in sync()-------------");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }


    public void syncAndSleep() throws InterruptedException {
        synchronized (lock){
            System.out.println("------------take time sync()-------------");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());

            Thread.sleep(5000);

        }
    }

    public void printLockObjectHeader(){
        System.out.println("Thread:" + Thread.currentThread().getName() + ";" +ClassLayout.parseInstance(lock).toPrintable());
    }

    public void calculateHashAndPrint(){
        System.out.println("Calculate Hash:" +Integer.toHexString(lock.hashCode()));

        System.out.println("After invoke hashcode, print Object again");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }

}

Monitor类

Monitor类是测试中用作对象锁的类简单的继承了Object类,仅可能会对hashCode方法做一些修改(用来验证MarkWord中哈希值的相关信息)。

public class Monitor {
    
    //我们可能处于特定的测试目的考虑,会注释掉这个方法,使用仍使用父类的方法
    @Override
    public int hashCode() {
        //必须要调用父类的hashCode方法 mark work中才会存hashCode
        return 0xff;
    }
}

SynchronizedUpgradeTest类

测试主入口,内部的几个方法之后几个测试的内容,之后我们将会依次运行这些方法对比对象头的信息:

public class SynchronizedUpgradeTest {
    static Foo foo = new Foo();

    public static void main(String[] args) throws InterruptedException {
        hashCodeTest();
//        biasedLock();

//        biasedLockInvalidAfterCalculate();

//        biasedLockUpgradeToLightLock();

//        lightLockToWeightLock();
    }

    protected static void hashCodeTest(){
        foo.printLockObjectHeader();
        foo.calculateHashAndPrint();
    }

    /**
     * JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
     */
    protected static void biasedLock(){

        foo.printLockObjectHeader();

        foo.sync();

        System.out.println("Exit sync()");
        foo.printLockObjectHeader();
    }

    /**
     * JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
     */
    protected static void biasedLockInvalidAfterCalculate(){
        foo.printLockObjectHeader();
        foo.calculateHashAndPrint();
        foo.sync();
        System.out.println("out sync()");
        foo.printLockObjectHeader();
    }

    /**
     * JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
     */
    protected static void biasedLockUpgradeToLightLock(){
        foo.printLockObjectHeader();
        foo.sync();
        System.out.println("out sync()");
        foo.printLockObjectHeader();

        System.out.println("---Another Thread Use Biased Lock");
        Thread thread = new Thread(()->{
            foo.sync();
            System.out.println("---Another Thread Out sync");
            foo.printLockObjectHeader();
        });
        thread.start();
    }
    /**
     * JVM OPTIONS: -XX:UseBiasedLocking -XX:BiasedLockingStartupDelay=10
     */
    protected static void lightLockToWeightLock() throws InterruptedException {
        foo.printLockObjectHeader();

        new Thread(()->{
            try {
                foo.syncAndSleep();
                foo.printLockObjectHeader();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(1000L);

        foo.sync();
        foo.printLockObjectHeader();
    }
}

以上全部测试代码都可以在我的GitHub中找到。同时为了保证测试的正确性,需要确保虚拟机运行参数和测试方法上的配置一致!

开始测试

测试1:MarkWord中哈希值——hashCodeTest()

image
可以从测试的图片中看到,对象锁MarkWord中关于锁的那几位确实是101,说明是偏向锁没错。但是在无论在调用hashCode前还是后的打印,MarkWord中都没有记录对象的Hash值。这似乎和我们值钱了解到的不太一样。其实这是因为我们重写了hashCode()方法。只有调用原生的hashCode()才会将哈希值记录在MarkWord中!
为了验证我们的猜测,我们注释掉Monitor类中的hashCode()方法,在测试一次。
image
当我们使用Object中的hashCode()方法时,MarkWord确实保存了哈希值。但是,另一个有趣的事情发生了,偏向锁直接升级成了轻量级锁。

测试2:偏向锁上锁与解锁测试——biasedLock()

image
从这里结果我们可以看出,解释线程释放了偏向锁,偏向锁依旧保存着线程的ID。

测试3:偏向锁升级为轻量级锁——biasedLockUpgradeToLightLock()

image

当偏向锁被标记过后,另一个线程再去获取锁时,锁会被升级成轻量级锁。并且在解锁后,也没有重新回到偏向锁的状态。

测试4:轻量级锁膨胀为重量锁———lightLockToWeightLock()

测试开始前,我们通过JVM参数设置让偏向锁一开始先不生效。
image
从测试结果中,我们可以看到轻量级锁膨胀为重量锁的过程。并且MarkWord中记录的信息也由栈帧的指针改为了monitor的指针。

原文地址:https://www.cnblogs.com/insaneXs/p/13378994.html