• JAVA多线程基础篇-关键字synchronized


    1.概述

    syncronized是JAVA多线程开发中一个重要的知识点,涉及到多线程开发,多多少少都使用过。那么syncronized底层是如何实现的?为什么加了它就能实现资源串行访问?本文将基于上述问题,探索syncronized的用法及原理。由于本节知识涉及到JMM相关知识,具体可查看上一篇文章JAVA多线程基础篇-关键字volatile

    2.关键字syncronized的作用

    2.1 JAVA并发编程中三大问题

    1.原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其它因素干扰而中断,要么所有的操作都不执行;

    2.有序性:指的程序中代码的执行顺序,JAVA在编译时和运行时会对代码进行优化,导致程序最终的执行顺序不一定就是我们编写代码时的顺序;

    3.可见性:当一个线程对共享变量进行了修改,其它线程能够立即看到修改后的值。

    2.2 syncronized如何解决并发中三大问题

    1.syncronized保证原子性:syncronized能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果;
    2.syncronized保证有序性:加关键字syncronized后,依然会发生指令重排序,由于有同步代码块,可以保证同一时间只有一个线程执行同步代码中的代码;
    3.syncronized保证可见性:执行syncronized时,会对应lock原子操作,刷新工作内存中共享变量的值。

    2.3 synchronized的使用场景

    使用位置作用范围作用对象案例代码
    方法普通实例方法类的实例对象public synchronized void test() {}
    静态方法当前类对象方法区中的类对象public static synchronized void test() {}
    Class对象类对象该Class对象synchronized(synchronizedDemo.class){}
    任意实例对象 object实例对象该object对象synchronized(object) {}

    由上表可知,synchronized的具体功能包括:锁方法、锁对象和锁代码块。

    2.4 synchronized的特性

    2.4.1 可重入性

    1.概念
    一个线程可以多次执行synchronized,重复获取同一把锁。
    2.案例

    public class RecursionTest {
    
        public static void main(String[] args) {
            new syncThread().start();
            new syncThread().start();
        }
    }
    
    class syncThread extends Thread {
    
        public void run() {
            synchronized (RecursionTest.class) {
                System.out.println(Thread.currentThread().getName() + "进入同步代码块1");
                synchronized (RecursionTest.class) {
                    System.out.println(Thread.currentThread().getName() + "进入同步代码块2");
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行结果如下:
    在这里插入图片描述
    由上述代码可知,Thread-0分别打印并输出了同步代码块1和同步代码块2,说明获取并执行synchronized (RecursionTest.class) 这个代码两次,两次获取到同一把锁。

    3.原理
    synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。每次执行完同步代码块时,计数器的数量会减1,直到计算器为0才会释放这个锁。

    4.优点

    1.可以避免死锁;
    2.可以更好地封装代码。

    5.不可中断
    一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直处于阻塞或等待,不可被中断。

    2.5 synchronized原理

    2.5.1 JAVA对象构成

    要了解synchronized的工作原理,首先要了解JAVA对象的构成。对象在内存中的存储布局可以分为三个部分,分别是对象头、示例数据和填充数据。

    存储区域详细说明
    实例数据存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐
    填充数据由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
    对象头在HotSpot虚拟机中,对象头又被分为两部分,分别为:Mark Word(标记字段)、Klass Pointer(类型指针)。如果是数组,那么还会有数组长度

    接下来详细分析一下对象头,这块内容比较重要,也与我们今天的关键字synchronized息息相关。

    • Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
    • Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    每个对象实例都会关联一个Monitor对象,因此它也被称为管程或者监视器锁。Monitor对象既可以与对象一起创建销毁,也可以在线程视图获取对象锁时自动生成,当这个Monitor对象被线程持有时,它便处于锁定状态。关联关系图如下:
    在这里插入图片描述
    在HotSpot虚拟机中,Monitor是由ObjectMonitor的类实现的(底层C++),该类有很多属性,核心的属性主要包含以下几个:

    • _ower :用来指向持有monitor的线程,它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识,等到线程释放锁后_ower又会被重置为NULL;
    • _WaitSet :调用了锁对象的wait方法后的线程会被加入到这个队列中;
    • _cxq:是一个阻塞队列,线程被唤醒后根据决策判断是放入cxq还是EntryList;
    • _EntryList:没有抢到锁的线程会被放到这个队列;
    • count: 用于记录线程获取锁的次数,成功获取到锁后count会加1,释放锁时count减1。

    ObjectMonitor的完整属性如下,来源于openjdk 8的jvm源码:

    objectMonitor.cpp
      ObjectMonitor() {
        _header       = NULL;
        _count        = 0;   \\用来记录获取该锁的线程数
        _waiters      = 0,
        _recursions   = 0;    \\锁的重入次数
        _object       = NULL;
        _owner        = NULL;  \\当前持有ObjectMonitor的线程
        _WaitSet      = NULL;  \\wait()方法调用后的线程等待队列
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ; \\阻塞等待队列
        FreeNext      = NULL ;
        _EntryList    = NULL ; \\synchronized 进来线程的排队队列
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;  \\自旋计算
        OwnerIsThread = 0 ;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.5.2 对象头解析

    对象头包括Mark Word和Klass Pointer。
    1.Mark Word
    下面是Hotspot对象头:
    在这里插入图片描述
    由上图可知,针对无锁状态和偏向锁,锁的标志位都是一样的,所以增加一个偏向锁位来进行区分(1:表示偏向锁,0:无锁状态)。

    2.Klass Pointer
    这一部分空间主要用来存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64的JVM为64位。
    如果应用的对象过多,使用64位的指针将会浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项-xxX:+UseCompressedoops开启指针压缩,其中,oop即ordinary objectpointer普通对象指针。开启该选项后,下列指针将压缩至32位:

    (1)每个Class的属性指针(即静态变量);
    (2)每个对象的属性指针(即对象变量)3.普通对象数组的每个元素指针;
    (3)一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

    在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
    在64位系统中,Mark Word = 8 bytes,类型指针 = 8 bytes,对象头 = 16 bytes = 128 bits;

    3.原理分析

    3.1 修饰同步代码块

    查看如下代码:

    public class JAVAPSyncTest {
        public static void main(String[] args) {
            syncTest();
        }
    
        public static void syncTest() {
            synchronized (JAVAPSyncTest.class) {
                System.out.println("hello ");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    利用javap指令反汇编上述代码,得到如下结果:

    "C:\Program Files\java\jdk1.8.0_162\bin\javap.exe" -v com.eckey.lab.sync.JAVAPSyncTest
    Classfile /F:/ideaworkspace/springboot-demos/springboot-juc/target/classes/com/eckey/lab/sync/JAVAPSyncTest.class
      Last modified 2022-6-22; size 779 bytes
      MD5 checksum b9991d2649bdb91fd9e81ec14f00837f
      Compiled from "JAVAPSyncTest.java"
    public class com.eckey.lab.sync.JAVAPSyncTest
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#26         // java/lang/Object."<init>":()V
       #2 = Methodref          #3.#27         // com/eckey/lab/sync/JAVAPSyncTest.syncTest:()V
       #3 = Class              #28            // com/eckey/lab/sync/JAVAPSyncTest
       #4 = Fieldref           #29.#30        // java/lang/System.out:Ljava/io/PrintStream;
       #5 = String             #31            // hello
       #6 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #7 = Class              #34            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               Lcom/eckey/lab/sync/JAVAPSyncTest;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               MethodParameters
      #20 = Utf8               syncTest
      #21 = Utf8               StackMapTable
      #22 = Class              #34            // java/lang/Object
      #23 = Class              #35            // java/lang/Throwable
      #24 = Utf8               SourceFile
      #25 = Utf8               JAVAPSyncTest.java
      #26 = NameAndType        #8:#9          // "<init>":()V
      #27 = NameAndType        #20:#9         // syncTest:()V
      #28 = Utf8               com/eckey/lab/sync/JAVAPSyncTest
      #29 = Class              #36            // java/lang/System
      #30 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
      #31 = Utf8               hello
      #32 = Class              #39            // java/io/PrintStream
      #33 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
      #34 = Utf8               java/lang/Object
      #35 = Utf8               java/lang/Throwable
      #36 = Utf8               java/lang/System
      #37 = Utf8               out
      #38 = Utf8               Ljava/io/PrintStream;
      #39 = Utf8               java/io/PrintStream
      #40 = Utf8               println
      #41 = Utf8               (Ljava/lang/String;)V
    {
      public com.eckey.lab.sync.JAVAPSyncTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/eckey/lab/sync/JAVAPSyncTest;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=0, locals=1, args_size=1
             0: invokestatic  #2                  // Method syncTest:()V
             3: return
          LineNumberTable:
            line 5: 0
            line 6: 3
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       4     0  args   [Ljava/lang/String;
        MethodParameters:
          Name                           Flags
          args
    
      public static void syncTest();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=0
             0: ldc           #3                  // class com/eckey/lab/sync/JAVAPSyncTest
             2: dup
             3: astore_0
             4: monitorenter
             5: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
             8: ldc           #5                  // String hello
            10: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            13: aload_0
            14: monitorexit
            15: goto          23
            18: astore_1
            19: aload_0
            20: monitorexit
            21: aload_1
            22: athrow
            23: return
          Exception table:
             from    to  target type
                 5    15    18   any
                18    21    18   any
          LineNumberTable:
            line 9: 0
            line 10: 5
            line 11: 13
            line 12: 23
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 18
              locals = [ class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121

    由上述反汇编代码可知,System.out.println("hello ");行对应的代码,分别被monitorenter和moniterexit两条指令包围。如下图所示:
    在这里插入图片描述
    中间的指令执行的就是打印hello的语句。出现这种情况的原因是:
    每一个对象都会和一个监视器Monitor关联,监视器被占用时会被锁住,其它线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获得当前对象对应的monitor所有权,其过程如下:

    (1)若monitor的进入数为0,线程可以进入monitor,并将monitor的进入设置为1,当前线程成为monitor的owner(所有者);
    (2)若线程已拥有monitor的所有权,允许它能重入monitor,则进入monitor的进入数加1,也就是_recursions属性加1;
    (3)若其它线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

    3.2 修饰同步方法

    案例代码如下:

    public class JAVAPSyncTest {
        public static void main(String[] args) {
            syncTest();
        }
    
        public static synchronized void syncTest() {
                System.out.println("hello ");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    利用javap反编译得到结果如下:

    "C:\Program Files\java\jdk1.8.0_162\bin\javap.exe" -v com.eckey.lab.sync.JAVAPSyncTest
    Classfile /F:/ideaworkspace/springboot-demos/springboot-juc/target/classes/com/eckey/lab/sync/JAVAPSyncTest.class
      Last modified 2022-6-23; size 672 bytes
      MD5 checksum a75c1ee1c8a796d3ee3db04ff54afe9f
      Compiled from "JAVAPSyncTest.java"
    public class com.eckey.lab.sync.JAVAPSyncTest
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
       #2 = Methodref          #6.#24         // com/eckey/lab/sync/JAVAPSyncTest.syncTest:()V
       #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
       #4 = String             #27            // hello
       #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #6 = Class              #30            // com/eckey/lab/sync/JAVAPSyncTest
       #7 = Class              #31            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               Lcom/eckey/lab/sync/JAVAPSyncTest;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               MethodParameters
      #20 = Utf8               syncTest
      #21 = Utf8               SourceFile
      #22 = Utf8               JAVAPSyncTest.java
      #23 = NameAndType        #8:#9          // "<init>":()V
      #24 = NameAndType        #20:#9         // syncTest:()V
      #25 = Class              #32            // java/lang/System
      #26 = NameAndType        #33:#34        // out:Ljava/io/PrintStream;
      #27 = Utf8               hello
      #28 = Class              #35            // java/io/PrintStream
      #29 = NameAndType        #36:#37        // println:(Ljava/lang/String;)V
      #30 = Utf8               com/eckey/lab/sync/JAVAPSyncTest
      #31 = Utf8               java/lang/Object
      #32 = Utf8               java/lang/System
      #33 = Utf8               out
      #34 = Utf8               Ljava/io/PrintStream;
      #35 = Utf8               java/io/PrintStream
      #36 = Utf8               println
      #37 = Utf8               (Ljava/lang/String;)V
    {
      public com.eckey.lab.sync.JAVAPSyncTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/eckey/lab/sync/JAVAPSyncTest;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=0, locals=1, args_size=1
             0: invokestatic  #2                  // Method syncTest:()V
             3: return
          LineNumberTable:
            line 5: 0
            line 6: 3
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       4     0  args   [Ljava/lang/String;
        MethodParameters:
          Name                           Flags
          args
    
      public static synchronized void syncTest();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=0, args_size=0
             0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #4                  // String hello
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 9: 0
            line 10: 8
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    由上述反汇编代码可知,利用synchronized修饰同步代码块,并没有出现monitorenter和moniterexit来修饰,这又是为啥?原因在于同步方法在被synchronized修饰后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和moniterexit。再执行完同步方法前会调用monitorenter,在执行完同步方法后会调用moniterexit。

    3.3 synchronized锁升级过程

    synchronized锁在JDK 1.5到JDK 1.6时进行了一个升级,HotSpot虚拟机开发团队花了大量的时间进行了锁的优化,包括偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、适应性自旋(Adaptive Spining)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)等,这些技术都是为了在线程之间更高效地共享数据、以及解决竞争问题,从而提高程序执行效率。
    锁升级的主要流程如下:
    在这里插入图片描述

    (1) jvm会有4s的偏向锁开启延迟时间,在这个偏向延迟内对象处于为无锁态。如果关闭偏向锁启动延迟、或是经过4秒且没有线程竞争对象的锁,那么对象会进入无锁可偏向状态。准确来说,无锁可偏向状态应该叫做匿名偏向(Anonymously biased)状态,因为这时对象的mark word中后三位已经是101,但是threadId指针部分仍然全部为0,它还没有向任何线程偏向。综上所述,对象在刚被创建时,根据jvm的配置对象可能会处于 无锁匿名偏向两个状态;
    (2) 在无锁不可偏向的情况下,如果有线程获取锁,则会直接由无锁状态变为轻量级锁状态;
    (3)偏向锁升轻量级锁,这一过程通常伴随另一个线程获取到了锁,也就意味着不仅仅只有当前一个线程来通过锁访问资源;
    (4)轻量级锁升重量级,一般是多个线程竞争资源导致(重度竞争),偏向锁也可通过调用wait和hashCode方法来获取重量级锁(耗时过长wait)。

    3.3.1 偏向锁原理

    1.偏向锁使用场景
    偏向锁是JDK1.6中引进的,由于大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。一旦出现多个线程竞争时,偏向锁就会被撤销。
    2.偏向锁原理
    当锁对象第一次被获取的时候,虚拟机将会把对象头中的标志位设为"01"(偏向模式),同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁效率高。
    3.偏向锁的撤销

    1.偏向锁的撤销动作必须等待全局安全点;
    2.暂停拥有偏向锁的线程,判断对象是否处于被锁定状态;
    3.撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

    偏向锁是在应用程序启动几秒钟之后才激活,可以使用-xXx:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过xx:-UseBiasedLocking=false参数关闭偏向锁。
    全局安全点指的是某一时刻线程全部停止。

    3.偏向锁的优势
    偏向锁是在只有一个线程执行同步块时提高性能,适用于一个线程反复获得同一把锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

    3.3.2 轻量级锁(自旋锁)

    1.什么是轻量级锁
    轻量级锁是JDK 1.6之中加入的新型锁机制,“轻量级”是相对于使用monitor传统锁而言的,因此传统的锁机制就称为"重量级"锁。"轻量级锁"并不是用来代替重量级锁的,引入轻量级锁能够避免重量级锁引起的性能消耗。如果多个线程在同一时刻进入临界区,会导致锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
    2.轻量级锁原理
    轻量级锁的升级步骤如下:

    1.判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则VM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced
    Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将LockReocrd中的owner指向当前对象;
    2.JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作;
    3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

    3.轻量级锁出现场景
    锁内部执行的代码较少,能够快速执行完成,获取锁的时间不用太长。
    4.为什么有轻量级锁还需要重量级锁
    轻量级锁(自旋锁)是消耗CPU资源的,如果锁的时间较长,或者自旋线程较多,会占用大量CPU资源。重量级锁有等待队列,所有未获取到锁的线程会进入队列等待,不需要消耗CPU资源。
    5.偏向锁是否一定比轻量级锁效率高
    不一定。在明确知道会有多线程竞争的情况下,偏向锁会涉及锁撤销,消耗大量性能。所有如果一开始就知道涉及线程竞争资源,应使用轻量级锁。
    6.锁优化-适应性自旋(Adaptive Spining)
    从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。
    其中解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
    7.锁消除
    锁消除即删除不必要的加锁操作。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。如下面这段代码:

    public class LockReleaseDemo {
        public static void main(String[] args) {
            String str = appendStr();
            System.out.println(str);
        }
    
        public static synchronized String appendStr() {
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("aa");
            stringBuffer.append("bb");
            stringBuffer.append("cc");
            return stringBuffer.toString();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。因此synchronized 修饰可去除。

    8.锁粗化

    锁粗化指的是将多次连接在一起的加锁、解锁操作合并为异常,并将多个连续的锁扩展成一个范围更大的锁。见下面代码:

     public static void main(String[] args) {
            StringBuffer stringBuffer = new StringBuffer();
            for (int i = 0; i < 100; i++) {
                stringBuffer.append("a");
            }
    
            System.out.println(stringBuffer.toString());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    由于StringBuffer 的append方法被synchronized 修饰,for循环中执行100次append,按照理论会进行100次加锁解锁。事实上,JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需加锁一次即可。上述代码JVM会自动优化,在for循环外加一次锁。

    3.3.3 重量级锁

    一旦从轻量级锁切换为重量级锁,意味着所有获取锁的线程都会被防置在一个队列中,线程的阻塞和唤醒需要CPU从用户态切换到内核态,频繁的阻塞和唤醒对CPU来说是很耗费性能的一个操作。

    4.小结

    1.synchronized是一个关键字,底层全部由JVM帮助实现,且在1.6之后进行了一系列优化,性能有了极大地提升;
    2.synchronized锁升级过程是逐级递增,包含无锁、偏向锁、轻量级锁和重量级锁等四种锁状态;
    3.synchronized和Lock都可以加锁,一个是JVM层面,一个是JDK层面,synchronized会自动释放锁,而Lock必须手动释放锁;
    4.synchronized能锁住方法和代码块,而Lock只能锁住代码块;
    5.synchronized使用时应尽量较少synchronized的范围(减少synchronized代码块内部代码)、降低synchronized锁的粒度(锁拆分)、读写分离(读取时不加锁,写入和删除时加锁)。

    5.参考文献

    1.https://www.bilibili.com/video/BV1aJ411V763
    2.https://juejin.cn/post/6844903600334831629
    3.https://www.bilibili.com/video/BV1tz411q7c2
    4.https://juejin.cn/post/6844904196676780040

  • 相关阅读:
    linux服务器在没有网的条件下,怎么安装使用numpy呢
    深入解读Dubbo:微服务RPC框架的佼佼者
    【Unity】【VR】如何用键鼠模拟VR输入
    c语言范例实例
    hive 中少量数据验证函数的方法-stack
    英国博士后招聘|约克大学—核磁共振监测催化
    Spring Boot 中使用 JSON Schema 来校验复杂JSON数据
    Agri-Net最短网络
    csp 202109-2 非零段划分
    sessionStorage和localStorage 的区别和使用,具体与 session 区分
  • 原文地址:https://blog.csdn.net/qq_33479841/article/details/124648239