likes
comments
collection
share

从源码全面解析 synchronized 关键字的来龙去脉

作者站长头像
站长
· 阅读数 39
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

深入理解 synchronized

一、引言

对于 Java 开发者而言,关于 并发编程,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 并发编程 解析系列文章,将带你领略 并发编程 的奥秘

废话不多说,发车! 从源码全面解析 synchronized 关键字的来龙去脉

本文流程图可关注公众号:爱敲代码的小黄,回复:synchronized 获取,或者添加博主WX进群领取 贴心的小黄为大家准备的文件格式为 POS文件,方便大家直接导入 ProcessOn 修改使用

二、synchronized基本使用

synchronized 的使用一般就是 同步方法同步代码块

1、同步方法

1.1 静态方法

public synchronized static void show() {
    System.out.println("我是show方法");
}

1.2 非静态方法

public synchronized void showStatic() {
    System.out.println("我是showStatic方法");
}

1.3 区别

静态方法:锁的是当前的类

非静态方法:锁的是当前的对象

我们来个例子模拟一下:

public class SynchronizedTest {
    public static void main(String[] args) {
        // 直接执行MyTest类静态方法【这里锁的是MyTest类】
        MyTest.showStatic();

        MyTest myTest = new MyTest();
        // 创建对象,执行其show方法【这里锁的是myTest这个对象】
        myTest.show();

    }
}

class MyTest {
    public synchronized  void show() {
        System.out.println("我是show方法");
    }

    public synchronized static void showStatic() {
        System.out.println("我是showStatic方法");
    }
}

2、代码块

对于代码块来说,我们经常有下面两种写法:

synchronized (myTest){
    // 执行业务逻辑
}

synchronized (MyTest.class){
    // 执行业务逻辑
}

这种的写法也是上面我们说的,分别锁的是 对象

这块的基本使用就讲到这里,我们继续往后看

三、synchronized优化

1、背景

这里大家有没有一个疑惑,这个 synchronized 关键词为啥要优化?不优化不行嘛?

JDK1.5 的时候,Doug Lee 推出了 ReentrantLockReentrantLock 的性能远高于 synchronized,所以 JDK 团队就在 JDK1.6 中,对 synchronized 做了大量的优化。

简单来说,你 JDK 团队再不去优化,都去用 Doug Lee 定义的 ReentrantLock JDK 团队的脸还往哪里放

到这里,你是不是感觉 Doug Lee 这个哥们特别牛逼,我们看看这哥们的背景:gee.cs.oswego.edu/

为 Java 贡献了 HashMapjava.util.concurrent ,只能说:牛逼

2、优化维度

2.1 锁消除

synchronized 修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了 synchronized ,他也不会触发。

如下:

public synchronized void method(){
    // 没有操作临界资源
    // 此时这个方法的synchronized你可以认为木有~~
}

临界资源:一次仅允许一个进程使用的共享资源

2.2 锁膨胀

如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

public void method(){
    for(int i = 0;i < 999999;i++){
        synchronized(对象){

        }
    }
    
    // 这是上面的代码会触发锁膨胀
    synchronized(对象){
        for(int i = 0;i < 999999;i++){

        }
    }
}

2.3 锁升级

锁升级ReentrantLock 的实现,是先基于乐观锁的 CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronizedJDK1.6 之前,完全就是获取不到锁,立即挂起当前线程,所以 synchronized 性能比较差。

我们简单介绍一下锁升级的步骤,后面会详细介绍

  • 无锁、匿名偏向:当前对象没有作为锁存在。
  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
    • 如果是,直接拿着锁资源走。
    • 如果当前线程不是,基于 CAS 的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
  • 轻量级锁:会采用自旋锁的方式去频繁的以 CAS 的形式获取锁资源(采用的是自适应自旋锁
    • 如果成功获取到,拿着锁资源走
    • 如果自旋了一定次数,没拿到锁资源,锁升级。
  • 重量级锁:就是最传统的 synchronized 方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)

四、synchronized实现原理

4.1 字节码

我们从字节码层面解析一下 synchronized 做了什么

我们将上述代码:

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        synchronized (o) {
            System.out.println("111");
        }
    }
}

编译成 Class 文件,然后使用 javap -c SynchronizedTest.class 得到编译后的字节码文件

public class cn.hls.SynchronizedTest {
  public cn.hls.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    Code:
       0: new           #2                  // 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           #4                  // String 111
      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
}

JVM 保证一个 monitor 一次只能被一个线程占有,monitorentermonitorexit 是两个与监视器相关的字节码指令。

当线程执行 monitorenter 的时候尝试获取栈顶对象的监视器 monitor,也就是尝试获取锁,如果此时 monitor 没有被其他线程占用就获得锁,monitor 计数器设置为1,当线程已经获得 monitor 的所有权了,monitorenter 指令也会顺利执行,monitor 计数器+1.如果其他线程拥有 monitor 的所有权,当前线程会阻塞,直到 monitor 计数器变为0。

当线程执行 monitorexit 指令的时候,监视器计数器-1,计数器为0的时候锁被释放,其他等待的线程可以尝试获得 monitor 的所有权。

这里如果加的是方法锁,反编译获取字节码会得到 ACC_SYNCHRONIZED 的标记,这个标记在我们的 HotSpot 里面也会隐式的调用 monitorentermonitorexit

至于这里的 monitorentermonitorexit 底层原理如何,我们后续再讲。

4.2 Mark Word

在我们讲述 synchronized 原理之前,我们需要对对象的结构做一个大概的描述,毕竟 synchronized 是基于对象的。

从源码全面解析 synchronized 关键字的来龙去脉

这里我们主要关心对象头中的 Mark Word

从源码全面解析 synchronized 关键字的来龙去脉

大家可以停个几分钟,观察一下这个图

从上图我们基本可以猜测到,synchronized 关键字的实现,就是在对象的 Mark Word 里面做了一系列的标记,从而实现的锁升级。

五、深入Mark Word探寻锁升级

为了能在 IDEA 看到对象头的信息,我们需要导入下面的 maven 包:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

1、无锁

无锁状态是我们最经典的状态:

public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

如上,我们打印出其 MarkWord 构成:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们可以看到,其数值为:00000001 00000000 00000000 00000000,也就是 001 无锁状态。

1.1 偏向锁延迟

这里我们加上 synchronized 关键字

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

如上,我们打印出其 MarkWord 构成:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           48 f4 aa 02 (01001000 11110100 10101010 00000010) (44758088)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们可以看到,其数值为:01001000 11110100 10101010 00000010,也就是 000 轻量级锁状态。

这个时候可能有一些懵逼,怎么直接跳到轻量级锁了,不是说好的偏向锁呢

这里需要介绍一个技术,叫做:偏向锁延迟

偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启

因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

说人话:当我们的 JVM 启动的时候,偏向锁的撤销必须要等到一个安全点(STW),但这个安全点的出现会导致系统效率的低下,于是在启动的时候暂时不开启偏向锁,等到 4s 之后再开启偏向锁。

1.2 匿名偏向锁

当我们将上述代码改为如下:

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(6000);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

如上,我们打印出其 MarkWord 构成:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

你神奇的看到,在我们没有加 synchronized 的情况下,他竟然变成了 101 偏向锁。

原因:如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向

注意:这里的匿名偏向锁是没有存储线程的!

2、偏向锁

不论是由无锁状态升级到的偏向锁,还是匿名偏向锁

在偏向锁这一步做的功能也很简单,就是 MarkWord 保存了你当前线程的 ID

如果当前线程再次获取锁资源时,会从 MarkWord 中拉取到当前的线程 ID 并进行对比,若一致则放行

否则发生锁竞争,将当前的偏向锁升级为轻量级锁

2.1 偏向锁的撤销

偏向锁撤销的开销花费还是挺大的,其大概过程如下:

  • 在一个安全点停止拥有锁的线程。
  • 遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的 Mark Word,清除其线程ID。
  • 将当前锁升级成轻量级锁。
  • 唤醒当前线程。

所以,如果某些临界区存在两个及两个以上的线程竞争,那么偏向锁反而会降低性能。在这种情况下,可以在启动 JVM 时就把偏向锁的默认功能关闭。这也就是上面我们 偏向锁延迟 的原因

3、轻量级锁

当升级至轻量级锁时,这个时候会有一个变化:

从我们的栈帧中开辟一块内存,叫做:Lock Record,这个东西是做什么用的呢?

3.1 Lock Record的作用

从源码全面解析 synchronized 关键字的来龙去脉

当我们的线程抢占一个轻量级锁时,这个时候会自动在栈帧里面开辟一块空间:Lock Record,这个空间里面有两个参数:

  • owner:当前对象
  • Displaced Mark Word:当前对象的 Mark Word(这里会复制一份)

从源码全面解析 synchronized 关键字的来龙去脉

如果当前线程可以成功抢占该锁,对象的锁记录指针指向我们的 Lock Record,而 owner 则指向当前的 Mark Word

整体流程:

  • 虚拟机在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的 Mark Word 复制到锁记录中。
  • 拷贝成功后,虚拟机将使用CAS操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word
  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
  • 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

3.2 为什么需要开辟 Lock Record?

当一个对象被调用 HashCode() 方法时,JVM 会将该对象的 HashCode 存储到对象头中,保证每次调用的都一致,这时候无锁状态下是没有问题的。

但如果锁升级到偏向锁,HashCode 在对象头里面没有空间去存储了,所以这也是偏向锁不能和 HashCode 共同存在的原因。

所以,JVM 推出了轻量级锁的优化,在线程中开辟了 Lock Record 内存,里面存储着 Mard Word 的信息,不断的 CAS 请求对象头的线程指向。

如果可以成功,将 Mard Word 内的指针指向当前线程的 Lock Record 就OK了。

当最终完成之后,对象头撤销到无锁状态,这时候只需要将 Lock Record 再赋值过去就好了。

3.3 普通自旋锁 和 自适应自旋锁

普通自旋锁: 每次自旋次数是固定的,只有超过这个次数之后,才升级为重量级锁

自适应自旋锁:每次自旋的次数不是固定的,是基于上一次抢占到锁自旋的次数,由 JVM 自适应的去调整的

4、重量级锁

终于到了我们最后的重量级锁,从上面我们也应该可以看出来,重量级锁主要是基于 Monitor

那么我们直接看 HotSpot 关于 Monitor 的实现:

博主认为,你可以不懂源码,但这两个类一定要记住(面试吹牛使用)!

首先,我们看一下他的结构体:

//Monitor结构体
ObjectMonitor::ObjectMonitor() {  
    _header      = NULL;  
    
   	// 用来该线程获取锁的次数
    _count       = 0;
    
    // 等待中的线程数
    _waiters     = 0,  

    //线程的重入次数
    _recursions  = 0;      
    _object       = NULL;  
     
   //标识拥有该Monitor的线程
    _owner        = NULL;   

   //等待线程组成的双向循环链表
   _WaitSet             = NULL;  
   _WaitSetLock  = 0 ;  
   _Responsible  = NULL ;  
   _succ                = NULL ;  

   //多线程竞争锁进入时的单向链表
   cxq                  = NULL ; 
   FreeNext             = NULL ;  

   //_owner从该双向循环链表中唤醒线程节点
   _EntryList           = NULL ; 
   _SpinFreq            = 0 ;  
   _SpinClock           = 0 ;  
   OwnerIsThread = 0 ;  
}

这里我们只需要记住以下这几个就可以:

  • cxq:竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中。
  • EntryList:Cxq中那些有资格成为候选资源的线程被移动到 EntryList 中。
  • _WaitSet:某个拥有 ObjectMonitor 的线程在调用 Object.wait() 方法之后将被阻塞,然后该线程将被放置在 WaitSet 链表中。
  • _owner:标识拥有该Monitor的线程
  • _recursions:线程的重入次数(这个其实用处不大,写在这里主要是面试重入锁考的较多)

从源码全面解析 synchronized 关键字的来龙去脉

4.1cxq

在线程进入 Cxq 前,抢锁线程会先尝试通过 CAS 自旋获取锁,如果获取不到,就进入 Cxq 队列,这明显对于已经进入 Cxq 队列的线程是不公平的。所以,synchronized 同步块所使用的重量级锁是不公平锁。

4.2 EntryList

Owner 线程释放锁时,JVM 会从 Cxq 中迁移线程到 EntryList ,并会指定 EntryList 中的某个线程(一般为Head)为 OnDeck Thread(Ready Thread)

EntryList 中的线程作为候选竞争线程而存在。

这里大家可能有个疑问,为什么我们需要把锁竞争交给 OnDeck Thread

简单来说,这里是为了提升系统整体的吞吐量,大家这里想象一下 Kafka 大概就能懂了。

4.3 _WaitSet

如果 Owner 线程被 Object.wait() 方法阻塞,就转移到 WaitSet 队列中,直到某个时刻通过 Object.notify() 或者Object.notifyAll() 唤醒,该线程就会重新进入 EntryList 中。

4.4 整体流程

  • 我们线程刚进来时,会进入 Cxq 的队列中
  • 当我们的 owner 释放锁时,会将 Xcq 里面的线程放到 EntryList
  • 这个时候由 OnDeck Thread 去进行锁竞争,竞争失败的则继续留在 EntryList
  • 当调用 Object.wait() 会进入 _WaitSet 队列,只要被唤醒时,才会重新进入 EntryList

在重量级锁中没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。

从源码全面解析 synchronized 关键字的来龙去脉

六、深入 HotSpot 探寻锁升级

这里博主的技术有限,就不自己写了,搬了搬小米技术博客的文章:xiaomi-info.github.io/2020/03/24/…

6.1 monitor 竞争过程

  • 通过 CAS 尝试把 monitorowner 字段设置为当前线程。
  • 如果设置之前的 owner 指向当前线程,说明当前线程再次进入 monitor,即重入锁执行 recursions ++ , 记录重入的次数。
  • 如果当前线程是第一次进入该 monitor, 设置 recursions 为 1,_owner 为当前线程,该线程成功获得锁并返回。
  • 如果获取锁失败,则等待锁的释放。

执行 monitorenter 指令时 调用以下代码

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),"must be NULL or an object");
  // 是否使用偏向锁  JVM 启动时设置的偏向锁-XX:-UseBiasedLocking=false/true
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
      // 轻量级锁
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

slow_enter 方法主要是轻量级锁的一些操作,如果操作失败则会膨胀为重量级锁,过程前面已经描述比较清楚此处不在赘述。enter 方法则为重量级锁的入口源码如下

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  // 省略部分代码
  
  // 通过 CAS 操作尝试把 monitor 的_owner 字段设置为当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }

 // 线程重入,recursions++
  if (cur == Self) {
     _recursions ++ ;
     return ;
  }

   // 如果当前线程是第一次进入该 monitor, 设置_recursions 为 1,_owner 为当前线程
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }

    for (;;) {
      jt->set_suspend_equivalent();
        // 如果获取锁失败,则等待锁的释放;
      EnterI (THREAD) ;

      if (!ExitSuspendEquivalent(jt)) break ;
          _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }
}

6.2 monitor 等待

  • 当前线程被封装成 ObjectWaiter 对象 node,状态设置成 ObjectWaiter::TS_CXQ
  • for 循环通过 CASnode 节点 push_cxq 列表中,同一时刻可能有多个线程把自己的 node 节点 push_cxq列表中。
  • node 节点 push_cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁则通过 park 将当前线程挂起等待被唤醒。
  • 当该线程被唤醒时会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
// 省略部分代码
void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;

    // Try lock 尝试获取锁
    if (TryLock (Self) > 0) {
        // 如果获取成功则退出,避免 park unpark 系统调度的开销
        return ;
    }

    // 自旋获取锁
    if (TrySpin(Self) > 0) {
        return;
    }

    // 当前线程被封装成 ObjectWaiter 对象 node, 状态设置成 ObjectWaiter::TS_CXQ
    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

    // 通过 CAS 把 node 节点 push 到_cxq 列表中
    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

        // 再次 tryLock
        if (TryLock (Self) > 0) {
            return ;
        }
    }

    for (;;) {
        // 本段代码的主要思想和 AQS 中相似可以类比来看
        // 再次尝试
        if (TryLock (Self) > 0) break ;
        assert (_owner != Self, "invariant") ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }

        // 满足条件则 park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            // 通过 park 将当前线程挂起,等待被唤醒
            Self->_ParkEvent->park() ;
        }

        if (TryLock(Self) > 0) break ;
        // 再次尝试自旋
        if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break;
    }
    return ;
}

6.3 monitor 释放

当某个持有锁的线程执行完同步代码块时,会释放锁并 unpark 后续线程(由于篇幅只保留重要代码)。

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
  
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;

    // 直接绕过 EntryList 队列,从 cxq 队列中获取线程用于竞争锁
      if (QMode == 2 && _cxq != NULL) {
          w = _cxq ;
          ExitEpilog (Self, w) ;
          return ;
      }
    // cxq 队列插入 EntryList 尾部
      if (QMode == 3 && _cxq != NULL) {
          w = _cxq ;
          for (;;) {
             assert (w != NULL, "Invariant") ;
             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
             if (u == w) break ;
             w = u ;
          }
          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }

          ObjectWaiter * Tail ;
          for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
          if (Tail == NULL) {
              _EntryList = w ;
          } else {
              Tail->_next = w ;
              w->_prev = Tail ;
          }
      }

    // cxq 队列插入到_EntryList 头部
      if (QMode == 4 && _cxq != NULL) {
          // 把 cxq 队列放入 EntryList
          // 此策略确保最近运行的线程位于 EntryList 的头部
          w = _cxq ;
          for (;;) {
             assert (w != NULL, "Invariant") ;
             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
             if (u == w) break ;
             w = u ;
          }

          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }

          if (_EntryList != NULL) {
              q->_next = _EntryList ;
              _EntryList->_prev = q ;
          }
          _EntryList = w ;
      }

      w = _EntryList  ;
      if (w != NULL) {
          ExitEpilog (Self, w) ;
          return ;
      }
      w = _cxq ;
      if (w == NULL) continue ;

      for (;;) {
          assert (w != NULL, "Invariant") ;
          ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
          if (u == w) break ;
          w = u ;
      }

      if (QMode == 1) {
         // QMode == 1 : 把 cxq 倾倒入 EntryList 逆序
         ObjectWaiter * s = NULL ;
         ObjectWaiter * t = w ;
         ObjectWaiter * u = NULL ;
         while (t != NULL) {
             guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ;
             t->TState = ObjectWaiter::TS_ENTER ;
             u = t->_next ;
             t->_prev = u ;
             t->_next = s ;
             s = t;
             t = u ;
         }
         _EntryList  = s ;
         assert (s != NULL, "invariant") ;
      } else {
         // QMode == 0 or QMode == 2
         _EntryList = w ;
         ObjectWaiter * q = NULL ;
         ObjectWaiter * p ;
          // 将单向链表构造成双向环形链表;
         for (p = w ; p != NULL ; p = p->_next) {
             guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
             p->TState = ObjectWaiter::TS_ENTER ;
             p->_prev = q ;
             q = p ;
         }
      }

      if (_succ != NULL) continue;

      w = _EntryList  ;
      if (w != NULL) {
          guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
          ExitEpilog (Self, w) ;
          return ;
      }
   }
}

6.4 notify 唤醒

notify 或者 notifyAll 方法可以唤醒同一个锁监视器下调用 wait 挂起的线程,具体实现如下

void ObjectMonitor::notify(TRAPS) {
    CHECK_OWNER();
    if (_WaitSet == NULL) {
        TEVENT (Empty - Notify);
        return;
    }
    DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);

    int Policy = Knob_MoveNotifyee;

    Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
    ObjectWaiter *iterator = DequeueWaiter();
    if (iterator != NULL) {
        // 省略一些代码

         // 头插 EntryList
        if (Policy == 0) {
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                List->_prev = iterator;
                iterator->_next = List;
                iterator->_prev = NULL;
                _EntryList = iterator;
            }
        } else if (Policy == 1) {      // 尾插 EntryList
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                ObjectWaiter *Tail;
                for (Tail = List; Tail->_next != NULL; Tail = Tail->_next);
                assert (Tail != NULL && Tail->_next == NULL, "invariant");
                Tail->_next = iterator;
                iterator->_prev = Tail;
                iterator->_next = NULL;
            }
        } else if (Policy == 2) {      // 头插 cxq
            // prepend to cxq
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                iterator->TState = ObjectWaiter::TS_CXQ;
                for (;;) {
                    ObjectWaiter *Front = _cxq;
                    iterator->_next = Front;
                    if (Atomic::cmpxchg_ptr(iterator, &_cxq, Front) == Front) {
                        break;
                    }
                }
            }
        } else if (Policy == 3) {      // 尾插 cxq
            iterator->TState = ObjectWaiter::TS_CXQ;
            for (;;) {
                ObjectWaiter *Tail;
                Tail = _cxq;
                if (Tail == NULL) {
                    iterator->_next = NULL;
                    if (Atomic::cmpxchg_ptr(iterator, &_cxq, NULL) == NULL) {
                        break;
                    }
                } else {
                    while (Tail->_next != NULL) Tail = Tail->_next;
                    Tail->_next = iterator;
                    iterator->_prev = Tail;
                    iterator->_next = NULL;
                    break;
                }
            }
        } else {
            ParkEvent *ev = iterator->_event;
            iterator->TState = ObjectWaiter::TS_RUN;
            OrderAccess::fence();
            ev->unpark();
        }

        if (Policy < 4) {
            iterator->wait_reenter_begin(this);
        }
    }
    // 自旋释放
    Thread::SpinRelease(&_WaitSetLock);

    if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
        ObjectMonitor::_sync_Notifications->inc();
    }
}

6.5 总结

说实话,看了这几个源码,感觉作用不太大.....

面试能吹到源码这一步,也是你牛逼了

个人感觉,记住上面的流程,再记住 parkunpark 就够了

LockSupport 的 park 和 unpark 是依赖 JVM(此处语境讨论 Hotspot)调用操作系统的 pthread_mutex_lock 和 pthread_cond_wait , 前者是保护后者和 counter 变量的互斥锁,保证只有一个线程操作 counter 变量和 condtion 上的等待队列

感兴趣的可以:pubs.opengroup.org/onlinepubs/…

七、流程图

本文流程图可关注公众号:爱敲代码的小黄,回复:synchronized 获取,或者添加博主WX进群领取 贴心的小黄为大家准备的文件格式为 POS文件,方便大家直接导入 ProcessOn 修改使用

从源码全面解析 synchronized 关键字的来龙去脉

八、总结

又是一篇大工程的文章结束了

记得校招的时候,知道一个锁升级,就感觉自己已经无敌了,转眼间,重新整理了一遍才发现 synchronized 这哥们这么难

但通过这篇文章,我相信,99% 的人应该都可以理解了 synchronized 的实现

没理解最后 HotSpot 源码的也不要灰心,因为博主也没理解......

那么如何证明你真的理解了 synchronized 呢,我这里出个经典的题目,大家可以想一下:说说synchronized锁升级的过程?

如果你能看到这,那博主必须要给你一个大大的鼓励,谢谢你的支持!

下期是 reentrantlock 源码文章,这个是 Java 层面的,应该还好,哈哈哈哈

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,Java领域新星创作者,喜欢后端架构和中间件源码。

我们下期再见。

转载自:https://juejin.cn/post/7215963267503882299
评论
请登录