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");
            }
        }
    }
}

运行结果如下:
在这里插入图片描述
由上述代码可知,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 ;
  }

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 ");
        }
    }
}

利用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
}

由上述反汇编代码可知,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 ");
    }
}

利用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
}

由上述反汇编代码可知,利用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();
    }
}

虽然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());
    }

由于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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值