zoukankan      html  css  js  c++  java
  • 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的指针。

  • 相关阅读:
    调用Android中的软键盘
    EditText图文混排
    android开源框架
    Android 菜单(OptionMenu)
    onRetainNonConfigurationInstance和getLastNonConfigurationInstance
    Android HttpClient基本使用方法
    Eclipse中文注释乱码解决
    mysql怎么定义外键
    javaproject积累——java 反射 invoke
    Floodlight 启动过程分析
  • 原文地址:https://www.cnblogs.com/insaneXs/p/13378994.html
Copyright © 2011-2022 走看看