欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

重温 JAVA -- synchronized 终

程序员文章站 2022-05-07 19:44:00
...

1、基本介绍

synchronized 关键字用于实现多线程之间的同步操作。
synchronized 可用于修饰 类方法, 对象方法, 代码块
使用 syncronized 时,需要有监视器。当修饰 类方法时, 监视器为该类; 当修饰 对象方法, 监视器为该对象; 当修饰 代码块, 需要手动传入监视器

1.1、类方法

示例代码

public class SynchronizedTest {
    public synchronized static void classMethod() {
    }

    public static void commonClassMethod() {
    }
}

反编译

  public static synchronized void classMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 12: 0

  public static void commonClassMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 15: 0

从反编译角度来看,加了 synchronized 关键字的,flags 多了 ACC_SYNCHRONIZED

1.2、对象方法

示例代码

public synchronized void objMethod() {
    }

    public void commonObjMethod() {
    }
}

反编译

  public synchronized void objMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0

  public void commonObjMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 15: 0

同类方法,被 synchronized 修饰的对象方法,flags 多了 ACC_SYNCHRONIZED 标识

1.3、代码块

示例代码

public class SynchronizedTest {

    public void codeBlock() {
        synchronized ("monitor") {
            System.out.println("codeBlock");
        }
    }
}

反编译

  public void codeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // String monitor
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String codeBlock
        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 10: 0
        line 11: 5
        line 12: 13
        line 13: 23
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class com/csp/boot/SynchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

可以看到在指令层面多加了 monitorenter, monitorexit。有一点需要注意:上面有 2个 monitorexit

为什么 1 个 synchronized 会产生 2 个 monitorexit 指令?

这是为了保证,方法在发生异常时,也能正常释放锁。

1.4、ACC_SYNCHRONIZED、monitorenter、monitorexit

monitorenter

每个对象都关联一个 monitor,当线程执行 monitorenter 时,monitor 会被锁定;
monitor 内部维护着一个被锁次数的计数器,对象未被锁定时,次数为0。线程执行 monitorenter 计数器会 +1。当同一个线程再次获得该对象的锁时,计数器会再加1。其他线程想获得该 monitor时,就会阻塞,直到计数器为0才能成功。

monitorexit

只有持有 monitor 的线程,才能执行该指令。
每次执行 monitorexit monitor 内部的计算器就会 -1。

ACC_SYNCHRONIZED

方法级别的锁是隐式的,当方法被 synchronized 标记时,在方法的常量池中会有 ACC_SYNCHRONIZED 标记。
一个方法在被调用时,需要先判断是否有 ACC_SYNCHRONIZED 标记;如果有则线程需要先持有 monitor 锁。当方法执行完毕时,会自动释放锁,不管方法执行成功,还是失败。

2、对象头

在对象的内存布局中,包含对象头,对象实例数据,对齐填充。其中,对象头的 mark word 记录了锁的信息。

mark word
重温 JAVA -- synchronized 终

3、锁升级

在早期,synchronized 是一个重量级锁,自从 jdk 1.6 版本之后,对 synchronized 锁进行了优化,引入了 偏向锁, 轻量锁

3.1、偏向锁

偏向锁是一种加锁,解锁都非常快速的锁。
大部分情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,偏向锁因此被引入

3.1.1、偏向锁加锁

场景一:锁对象未被线程持有

线程发现锁是匿名偏向状态(threadId 未指向任何线程),会使用 CASmark wordthreadId 指向本身。如果成功,则获取锁;否则加锁失败,升级为轻量锁。

场景二:获取偏向锁线程再次进入同步块

线程进入同步块时,发现 threadId 指向的就是自身。此时会往自身线程栈中添加一条 Displaced Mark Word 为空的 Lock Record。因为操作的是线程的私有变量,因此无需 CAS

注:Displaced Mark Word 是 JVM 为线程在栈帧中创建用于存储锁记录的空间

从场景一,场景二 来看,偏向锁的加锁效率是非常高的。

对于偏向锁而言,只要发生锁竞争,那么会立马升级为轻量锁

3.1.2、偏向锁解锁

解锁过程

偏向锁线程,在解锁时,仅仅是将自身线程栈中最后一条 Lock Recordobj 设置为 null

目的

未去修改 Mark WordthreadId,仅仅是为了同一个线程再次进入同步代码块时,无需使用 CAS。(偏向锁被引入的原因是:锁竞争情况少,且总是由同一个线程多次获得)

偏向锁解锁效率非常高

3.1.3、撤销偏向锁

未获取偏向锁线程进入同步块

未获取偏向锁线程进入同步块,发现 threadId 指向的不是自己,且是偏向状态,那么此时会进入到 撤销偏向锁的流程中。一般会在 safepoint 时,查看偏向锁线程是否存活。

  1. 偏向锁线程死亡,或不在同步代码块中,则将 mark word 设置为无锁状态。
  2. 偏向锁线程未死亡,且在同步代码块中。则将锁升级为轻量级锁,原偏向锁线程,继续持有锁。偏向锁线程释放锁时,按照轻量级锁方式释放。其他线程则自旋尝试获取轻量锁。

为什么在撤销偏向锁时,需要查看偏向锁线程是否死亡,或者在不在同步代码块中

因为,偏向锁在解锁时,仅仅是修改了自身栈中的值。

3.2、轻量锁

3.2.1、轻量锁加锁

流程

先将 Mark Word 复制一份到 Displaced Mark Word,然后用 CASMark Word 替换为自己栈中锁记录的指针。如果成功,则持有锁;失败则,自旋继续尝试获取锁。

自旋获取锁

轻量锁,不会无限次的自旋获取锁,而是在自旋一定次数后(自适应),如果还是未获取到锁,则将锁升级为重量锁

3.2.2、轻量锁解锁

解锁则是用 CASDisplaced Mark Word 复制回锁对象的 Mark Word。如果成功则表示没有竞争。如果失败,表示当前锁存在竞争,锁会膨胀为重量级锁

3.3、重量锁

当多个线程同时访问重量锁时,重量锁会设置几种状态区分线程(线程在请求重量锁失败后,会进入阻塞状态)
Contention List: 所有请求锁的线程,在请求锁失败后,会被放入该集合中。该集合的数据结构是先进后出
Entry List: Contention List 会让有机会成为候选人的线程放入到此集合中,主要是为了减少对 Contention List 的并发访问。
OnDeck: 任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Wait Set: 线程调用 wait 之后会被放入该队列中
Owner: 获得锁的线程被称为 Owner
!Owner: 释放锁的线程

3.3.1、执行流程

重温 JAVA -- synchronized 终

步骤1:线程在进入 Contention List 之前,线程会 CAS 尝试获取锁,获取不到才会进入 Contention List 尾部
步骤2:Owner 线程在解锁时,如果 Entry List 为空,那么会优先将 Contention List 部分线程移入 Entry List
步骤3:Owner 线程在解锁时,如果 Entry List 为空,那么会从 Contention List 队尾取一个线程成为 OnDeck。如果 Entry List 不为空,则从 Entry List 中选一个成为 OnDeckOnDeck 需要与还未进入 Contention List,还在自旋获取锁的线程进行竞争
步骤4:OnDeck 线程获得锁,成为 Owner 线程
步骤5:调用 wait 释放锁,进入 Wait Set 队列
步骤6:Onwer 线程执行 notifynotifyAllWait Set 中的某个线程或所有线程被唤醒

4、参考

Java面试之Synchronized解析
【大厂面试07期】说一说你对synchronized锁的理解?

相关标签: java