synchronized原理简析

fyh 2022年09月06日 48次浏览

加锁方式

vim synchronizedTest.java

编写代码

class synchronizedTest {
	public static void main(String args[]) {
	}

	public void a() {
		synchronized(synchronizedTest.class) {		
			System.out.println("this is a");
		}
	}

	public synchronized void b() {
			System.out.println("this is b");
	}

	public static synchronized void c() {
			System.out.println("this is c");
	}

	public void d() {
		Object obj = new Object();
		synchronized(obj) {
			System.out.println("this is d");
		}
	}
}

编译查看字节码

javac synchronizedTest.java
javap -v synchronizedTest.class
Classfile /C:/JAVA/synchronized/synchronizedTest.class
  Last modified 2022年9月5日; size 860 bytes
  MD5 checksum c9e99f4a391cec22646502d70b90e3a6
  Compiled from "synchronizedTest.java"
class synchronizedTest
  minor version: 0
  major version: 55
  flags: (0x0020) ACC_SUPER
  this_class: #2                          // synchronizedTest
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 6, attributes: 1
Constant pool:
   #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
   #2 = Class              #24            // synchronizedTest
   #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #27            // this is a
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = String             #30            // this is b
   #7 = Class              #31            // java/lang/Object
   #8 = String             #32            // this is c
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               a
  #16 = Utf8               StackMapTable
  #17 = Class              #33            // java/lang/Throwable
  #18 = Utf8               b
  #19 = Utf8               c
  #20 = Utf8               d
  #21 = Utf8               SourceFile
  #22 = Utf8               synchronizedTest.java
  #23 = NameAndType        #9:#10         // "<init>":()V
  #24 = Utf8               synchronizedTest
  #25 = Class              #34            // java/lang/System
  #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
  #27 = Utf8               this is a
  #28 = Class              #37            // java/io/PrintStream
  #29 = NameAndType        #38:#39        // println:(Ljava/lang/String;)V
  #30 = Utf8               this is b
  #31 = Utf8               java/lang/Object
  #32 = Utf8               this is c
  #33 = Utf8               java/lang/Throwable
  #34 = Utf8               java/lang/System
  #35 = Utf8               out
  #36 = Utf8               Ljava/io/PrintStream;
  #37 = Utf8               java/io/PrintStream
  #38 = Utf8               println
  #39 = Utf8               (Ljava/lang/String;)V
{
  synchronizedTest();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 3: 0

  public void a();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class synchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String this is a
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return
      Exception table:
         from    to  target type
             5    15    18   any
            18    21    18   any
      LineNumberTable:
        line 6: 0
        line 7: 5
        line 8: 13
        line 9: 23
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class synchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void b();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String this is b
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 12: 0
        line 13: 8

  public static synchronized void c();
    descriptor: ()V
    flags: (0x0029) 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           #6                  // String this is c
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8

  public void d();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #7                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #8                  // String this is d
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 12
        line 23: 20
        line 24: 30
      StackMapTable: number_of_entries = 2                                                                            ~                                             frame_type = 255 /* full_frame */                                                                                                                             offset_delta = 25                                                                                                                                           locals = [ class synchronizedTest, class java/lang/Object, class java/lang/Object ]                             ~                                           stack = [ class java/lang/Throwable ]                                                                                                                     frame_type = 250 /* chop */                                                                                                                                   offset_delta = 4                                                                                               全部
}

可以发现:

a()d()的策略是一样的,都具有monitorentermonitorexit的操作。

b()c()的策略是一样的,都在flags字段中添加了ACC_SYNCHRONIZED标记。

总结:

对于代码块加锁,使用的是monitor,而对于方法上修饰synchronized加锁,使用的是在flags中标记。

加锁对象

实例方法对象实例
静态方法Class实例
代码块传入synchronized的对象实例

JDK1.6优化

在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐填充。

重点关注对象头(MarkWord + Class Metadada Address + Array Length)中的MarkWord

Mark Word 用于存储对象自身的运行时数据,如 HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。占用内存大小与虚拟机位长一致(32位JVM -> MarkWord是32位,64位JVM -> MarkWord是64位)。

无锁

25bit4bit1bit(是否是偏向锁)2bit(锁标志位)
对象的hashCode对象分代年龄001

偏向锁

23bit2bit4bit1bit2bit
线程IDepoch对象分代年龄101

偏向锁的加锁

  1. 偏向锁标志是未偏向状态,使用 CAS 将 MarkWord 中的线程ID设置为自己的线程ID,
    1. 如果成功,则获取偏向锁成功。
    2. 如果失败,则进行锁升级。
  2. 偏向锁标志是已偏向状态
    1. MarkWord 中的线程 ID 是自己的线程 ID,成功获取锁
    2. MarkWord 中的线程 ID 不是自己的线程 ID,需要进行锁升级

偏向锁的锁升级需要进行偏向锁的撤销。

偏向锁的撤销

  1. 对象是不可偏向状态
    1. 不需要撤销
  2. 对象是可偏向状态
    1. MarkWord 中指向的线程不存活
      1. 允许重偏向:退回到可偏向但未偏向的状态
      2. 不允许重偏向:变为无锁状态
    2. MarkWord 中的线程存活
      1. 线程ID指向的线程仍然拥有锁
        1. 升级为轻量级锁,将 mark word 复制到线程栈中
      2. 不再拥有锁
        1. 允许重偏向:退回到可偏向但未偏向的状态
        2. 不允许重偏向:变为无锁状态

偏向锁指的就是JVM会认为只有某个线程才会执行同步代码(没有竞争的环境)。MarkWord会直接记录线程ID,只要线程来执行代码了,会比对线程ID是否相等,相等则当前线程能直接获取得到锁,执行同步代码。如果不相等,则用CAS来尝试修改当前的线程ID,如果CAS修改成功,那还是能获取得到锁,执行同步代码。如果CAS失败了,说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。

轻量级锁

30bit2bit
指向线程栈锁记录的指针00

轻量级锁状态下,当前线程会在栈帧下创建LockRecord,LockRecord 会把Mark Word的信息拷⻉进去,且有个Owner指针指向加锁的对象。线程执行到同步代码时,则用CAS试图将Mark Word的指向到线程栈帧的Lock Record,假设CAS修改成功,则获取得到轻量级锁;假设修改失败,则自旋(重试),自旋一定次数后,则升级为重量级锁。

重量级锁

30bit2bit
指向锁监视器的指针10

线程进入同步代码块/方法时,monitor对象就会把当前进入线程Id进行存储,设置MarkWord的monitor对象地址,并把阻塞的线程存储到monitor的等待线程队列中,它加锁是依赖底层操作系统mutex 相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。

mutex初始为0,表示锁可获取

加锁:monitorenter->计数器是否为0->计数器+1

解锁:monitorexit->是否为锁拥有者->计数器-1

为什么要采用锁升级机制

为什么切换用户态和内核态开销那么大

当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。

锁升级的意义

最根本的意义就是提升性能。

偏向锁和轻量级锁的设计理念就在于,期望短期只有较少的线程占有锁,占有锁的线程执行较短的时间便可以释放锁,因此可以使用CAS进行锁的替换,偏向锁和轻量级锁的主要性能开销就在于CAS自旋所耗费的CPU性能,短时间来说,这个性能消耗远小于用户态和内核态的切换。

但出现线程需要长时间的执行时,与其让等待的线程一直CAS长时间消耗CPU性能,不如切换用户态和内核态使用mutex加锁,让等待的线程进入挂起状态,等当前线程执行完毕再唤醒等待的线程。

部分内容转载自:https://www.cnblogs.com/wuqinglong/p/9945618.html