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

深入浅出synchronized的原理与源码 synchronized 阅读详情

深入理解 synchronized

一、引言

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

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

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

废话不多说,发车!

二、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 推出了 ReentrantLock,ReentrantLock 的性能远高于 synchronized,所以 JDK 团队就在 JDK1.6 中,对 synchronized 做了大量的优化。

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

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

为 Java 贡献了 HashMap 和 java.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尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized 在JDK1.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 一次只能被一个线程占有,monitorenter 和 monitorexit 是两个与监视器相关的字节码指令。

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

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

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

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

4.2 Mark Word

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

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

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

从上图我们基本可以猜测到,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的作用

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

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

如果当前线程可以成功抢占该锁,对象的锁记录指针指向我们的 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:线程的重入次数(这个其实用处不大,写在这里主要是面试重入锁考的较多)

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 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。

六、深入 HotSpot 探寻锁升级

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

6.1 monitor 竞争过程

  • 通过 CAS 尝试把 monitor 的 owner 字段设置为当前线程。
  • 如果设置之前的 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 循环通过 CAS 把 node 节点 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 总结

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

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

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

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

七、流程图

八、总结

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

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

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

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

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

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

Hotspot synchronized与volatile关键字实现(二) 源码解析 目录 一、synchronized底层实现 1、修饰代码块字节码分析 2、InterpreterGenerator::lock_method 3、执行本地方法synchronized解锁 4、解释执行普通Java方法synchronized解锁 二、jni_MonitorEnter /jni_MonitorExit 1、jni_MonitorEnter 2、jni_Moni... 阅读详情

相关推荐

深入解析Java中的synchronized关键字

接下来是偏向锁,我们还是用同样的代码即可,需要注意的是偏向锁必须在jvm启动后的一段时间才会运行,所以如果我们想打印偏向锁必须让线程休眠那么几秒,这里笔者就偷懒了一下,通过设置jvm参数。打印结果如下,我们只需关注第一行的object header,可以看到第一列的00000001,我们看到后3位为001,偏向锁标记为0,锁标记为01,001这就是我们说的无锁状态。上文讲解锁升级的之后,我们一直在说对象头的概念,所以为了能够直观的看到锁对象中对象头锁标记和锁状态的变化,我们这里引入一个jol工具。

禅与计算机程序设计的艺术 4907

synchronized源码解析

偏向锁、无锁、轻量锁、重量锁源码级解析

ckiuick的博客 956

(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现

偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。最后唤醒暂停的线程。通过观察大家会发现,如果我们启动了偏向锁,会执行 ObjectSynchronizer::fast_enter的逻辑,而如果我们没有开启偏向锁,则执行 ObjectSynchronizer::slow_enter逻辑,绕过偏向锁,直接进入轻量级锁。否则,锁膨胀成重量级锁,实现重量级锁的释放锁逻辑。

qq_66627105的博客 966

Java并发-Synchronized源码解析

可以保证或者在运行时,,同时它还可以。

weixin_49708955的博客 491

Hotspot synchronized与volatile关键字实现(一) 源码解析

一、synchronized用法 1、修饰实例方法 2、修饰静态方法 3、修饰代码块 二、synchronized底层实现 1、修改代码块的字节码分析 2、monitorenter 指令实现 3、InterpreterRuntime::monitorenter 4、monitorexit 指令实现 5、InterpreterRuntime::monitorexit 6、monitorenter/ monitorexit 指令的编译执行

菜鸟进阶之路 2234

深入源码解析synchronized关键字

摘要 本文深入剖析了JDK8中synchronized关键字的底层实现机制。通过ObjectMonitor结构体,详细阐述了线程竞争锁资源的流程:线程通过CAS获取锁,失败后自旋或进入阻塞队列;锁重入通过_recursions计数;调用wait的线程进入_WaitSet队列等待唤醒。文章结合源码分析了enter方法的上锁步骤,包括CAS竞争、自旋优化、锁升级等关键环节,并配以流程图辅助理解。最终指出synchronized通过CAS竞争和等待通知机制实现高效同步,在竞争激烈时主动挂起线程以减少CPU开销。

禅与计算机程序设计的艺术 100

synchronized底层源码

一、synchronized的使用场景 synchronized一般使用在下面这几种场景: 修饰代码块,指定一个加锁的对象,给对象加锁 public Demo1{ Object lock=new Object(); public void test1(){ synchronized(lock){ } } } 修饰静态方法,对当前类的Class对象加锁 public class Demo2 { //形式一 public void test1

huapro.vip的程序人生 1246

深析Synchronized关键字(小白慎入,深入jvm源码,两万字长文)

目录一、synchronized基础1.1synchronized的使用1.1示例1.2验证1.2.1 普通方法和代码块中使用this是同一个监视器(锁),即某个具体调用该代码的对象1.2.2 静态方法和代码块中使用该类的class对象是同一个监视器,任何该类的对象调用该段代码时都是在争夺同一个监视器的锁定1.2、synchronized的特点二、synchronized进阶2.1对象头2.2synchronized实现原理2.3锁升级2.3.1《java并发编程的艺术》的描述(引用)1.偏向锁(1)偏向锁的撤销(2)关闭偏向锁2.轻量级锁(1)轻量级锁加锁(2)轻量级锁解锁2.3.2一些补充

《吊打面试官系列》从源码全面解析 synchronized 关键字的来龙去脉

对于 Java 开发者而言,关于 并发编程,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。本期并发编程解析系列文章,将带你领略并发编程的奥秘。

爱敲代码的小黄 3047

关于石墨烯费米能级和偏置电压之间的关系问题

石墨烯费米能级与电压的关系问题

qq_37946542的博客 3514

用户视角下的vfsglobal预约困境:从注册到登录的全流程避坑指南

本文详细解析了vfsglobal签证预约系统从注册到登录的全流程操作,帮助用户避开常见技术陷阱。针对IP限制、DNS解析、浏览器缓存等登录障碍提供实用解决方案,并揭示账号管理的隐藏规则与支付环节的延时处理技巧,助力用户高效完成签证预约。

ol78901234的博客 871

Java 关键字:synchronized详解

synchronized 是 Java 中用于实现线程同步的关键字,它提供了一种简单且有效的方式来控制多个线程对共享资源的访问。 synchronized 的作用是确保在同一时刻只有一个线程可以进入被 synchronized 修饰的代码块或方法,从而避免多个线程同时修改共享数据而引发的数据不一致性和并发访问问题。

唯有努力,方可自信! 1万+

Unity海洋渲染实战:Ceto Ocean System核心原理与高级应用指南

实时水面渲染是计算机图形学中的重要课题,涉及波浪模拟、光照交互和着色器编程等核心技术。其原理通常基于物理模型,如使用Gerstner波模拟规则海浪,或通过快速傅里叶变换(FFT)处理频谱数据生成随机海面。这些技术的价值在于能以可控的性能开销,在游戏中实现具有体积感、反射和折射效果的真实水体。在开放世界、海战模拟等应用场景中,专业的海洋系统如Ceto Ocean System,通过模块化架构整合了波浪生成、动态网格和渲染管线,为开发者提供了完整的解决方案。本文深入探讨了其双引擎波浪系统和多层渲染结构,并分享了

weixin_29002595的博客 279

深入解析 Java 中的 synchronized 关键字

Java 的 synchronized 关键字是多线程编程中确保线程安全的核心机制。它通过互斥锁提供互斥性(一次只有一个线程访问共享资源)、可见性(线程间内存同步)和可重入性(同一线程可多次获取同一锁)。synchronized 是 JVM 内置的,简单易用,但灵活性较低,缺乏如超时锁或公平锁等高级功能。

持续输出Java相关知识 1389

QT实现串口通信

要实现串口通信,需要知道串口通信需要的信息 主要参数有:波特率、校验位、数据位、停止位、控制流 主要操作有:串口的打开和关闭、刷新设备串口、接发数据、开关显示灯等。 实现效果如图: 最后附赠完整源码。 第一步:在头文件中引入 QtSerialPort 类的两个头文件(必须引入) // 引入串口通信的两个头文件(第一步) #include <QtSerialPort/QSerialPort> // 提供访问串口的功能 #include <QtSerialPo

m0_49456900的博客 6万+

SAP BAPI调用避坑指南:处理MD61/MD62计划独立需求时,这些参数和返回值你必须检查

本文详细解析了SAP BAPI调用中处理MD61/MD62计划独立需求的关键参数和返回值检查要点,包括版本控制、日期类型和删除标记等核心参数设置,以及常见错误排查和高级调试技巧,帮助开发者高效管理需求计划数据。

weixin_30316097的博客 611

《Java源码分析》:Java中synchronized关键字的使用

1、当两个线程Thread1、Thread2访问一个对象obj的synchronized(this)代码块时,每个时刻都只能有一个访问此代码块,当Thread1访问时,Thread2只有在Thread1线程执行完这段代码块并释放锁后取得锁才能访问。2、当两个线程并发时,一个线程访问对象obj的synchronized(this)代码块时,其它的线程可以访问对象obj的非synchronized(this)代码块。

smart_an的专栏 796

在实战中学AI-车流量统计项目-YOLOV11车辆检测-YOLOV11模型的部署

考虑实际落地中部署推理资源消耗,速度与模型精度之间的均衡,同时,车辆属于比较通用场景的目标检测,我们将YOLOV11作为最终的模型选择,目前官方开源的模型基于coco数据集训练,里面已经包含了车辆相关类别,经测试,在监控视角下效果基本可用,所以,在落地过程中,我们先用这个模型来打通部署链路,后面再逐步优化模型性能。在Nvidia的硬件下做部署,部署框架优先选择官方框架TensorRT,下面重点讲述如何用TensorRT部署YOLOV11模型。

Aleo_优快云的博客 2167

(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现_objectsynchronizer fast_exit

偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。最后唤醒暂停的线程。通过观察大家会发现,如果我们启动了偏向锁,会执行 ObjectSynchronizer::fast_enter的逻辑,而如果我们没有开启偏向锁,则执行 ObjectSynchronizer::slow_enter逻辑,绕过偏向锁,直接进入轻量级锁。否则,锁膨胀成重量级锁,实现重量级锁的释放锁逻辑。

2401_84537540的博客 882

个人整理的数据集(手写中文数据、发票数据、快递单数据、车牌数据)

本人在工作生活中收集了各个方面比较多的真实的数据集如下: 一、手写中文数据集 1、档案类数据: 此数据集为手写档案数据,数量较大,大约128G,图像均未标注。 ex: 2、手写作文数据: 此数据集为手写作文数据,是大约800M左右,图像按行提供位置和内容标注,十分珍贵。 ex: 二、发票数据集: 1、增值税发票 此数据集包含了大量真实的增值税发票数据,大约9G,标注了发票位置数据,未提供详细标注。 ex: 2、购车发票 此数据集未提供标注。

weixin_43143224的博客 5877

信号编辑软件ZX/SignalEditing

信号编辑软件ZX/SignalEditing

weixin_45661395的博客 957

关键字之Synchronized

synchronized是通过软件(JVM)实现的,简单易用,即使在JDK5之后有了Lock,仍然被广泛的使用。使用Synchronized有哪些要注意的?锁对象不能为空,因为锁的信息都保存在对象头里作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错避免死锁。

杰尼龟的小龟壳的博客 1242

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

深入浅出学习 synchronized 关键字底层原理

synchronized 关键字底层原理解析 摘要 本文深入解析 Java 中 synchronized 关键字的底层实现原理。首先介绍其作为内置锁的三大特性:互斥性、可见性和有序性。然后分析三种基本用法及对应的锁对象,强调锁粒度由锁对象决定。 核心部分从字节码层面揭示 synchronized 的两种实现方式:代码块使用 monitorenter/monitorexit 指令,方法使用 ACC_SYNCHRONIZED 标志。深入剖析 Monitor 机制的工作原理,包括锁获取、释放流程及内部队列结构。最

学习和分享计算机相关知识,希望可以帮助到您 795

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

上一篇: 全栈真的那么难吗?我只能说是你没有这本阿里全栈笔记开源!
下一篇: 春招最新Java岗面试八股文必看,已帮541人拿下大厂offer!
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值