万字长文分析synchroized

690 阅读38分钟

1、前言

起因

虽然网络上不乏各种大佬,讲解synchronized的文章更是数不胜数,但是看一千遍不如做一遍,我希望将知识转化成自己的 (因此有了本文)。

目的:

本文尝试通俗易懂的讲清楚 synchronized ,我们将从线程安全讲起,引出synchronize的使用,然后对synchronized的字节码进行分析,然后是讲解Objectmonitor,再到各种锁(轻量级锁,偏向锁,重量级锁)的讲解,最后我们来个总结。希望和各位读者一起学习,进步!

2、多线程的利与弊&如何解决

2.1、使用多线程编程的优缺讨论

多线程编程在我们实际coding中,是很常见的一种编程方式,其 优点 显而易见,但任何事物都有两面性,他也有缺点,优/缺 点如下:

    1. 充分利用现代机器的 多核cpu ,使得资源利用最大化
    2. 多线程的处理方式,一般是要快于单线程的(当然特殊情况除外)
    3. 从使用者角度来讲,系统响应速度更快,体验更好
    4. ......
    1. 多个线程共享的资源,可能会出现线程安全问题
    2. 线程间切换,也是会消耗一定资源的
    3. 程序复杂性增加,编写以及排查时,需要一定成本
    4. 可能发生一些问题,如(死锁,cpu超过100% 等等异常现象)
    5. ......

其中缺点 2是使用多线程必然存在且基本无法解决的问题,而缺点 3缺点 4 在于开发者的水平和经验了。

缺点 1是使用多线程时候最常见的问题,并且也是必须要解决的,如果保证不了线程安全,那么不管你编写的代码,多么的快,多么的优雅,也将是三个字 :白忙活,因为大部分情况下 数据准确是必须要的!在保证数据准确的情况下,再谈代码的效率,速度等等内容,否则一切都是徒劳!

那么到底什么是线程安全,?为什么多线程场景下会出现线程安全问题?(虽然很多小伙伴知道这些已经被人们说烂了的概念,但是为了文章完整性,我们这里还是有必要说一下)下边让我们举个 🌰说明一下:

2.2、何为线程安全?如何保证?

我们一句话概括什么是线程安全:

当多个线程同时 对同一个共享资源进行非原子操作(如 :修改某个共享资源的数据时),将会出现线程安全问题。

Talk is cheap. Show me the code. 请看如下代码:


public class ThreadUnsafeTestByPool {

    //我的cpu颗数: 12
    private static int cpuCount = Runtime.getRuntime().availableProcessors();
    private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
          TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
    static {
       System.out.println("我的cpu颗数: "+cpuCount);
       threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
    }

    //成员变量,可被多个线程 共享
    private static long n = 0L;

    public static void main(String[] args) throws Exception {

       //多弄点,不然不好看出结果
       CountDownLatch countDownLatch = new CountDownLatch(10000);
       for (int i = 1; i < 10001; i++) {
          threadPool.execute(() -> {
             try {
                //synchronized (ThreadUnsafeTestByPool.class) {
                   n++;
                //}
                //System.out.println("当前提交的线程: "+Thread.currentThread().getName());
             } finally {
                countDownLatch.countDown();
             }
          });
       }
       //等待所有线程 执行完后再打印 ++的结果
       countDownLatch.await();
       System.out.println("最终++的结果: " + n);
    }

}

第一次运行控制台输出:
    最终++的结果: 9961
第二次运行控制台输出:
    最终++的结果: 9933
第三次运行控制台输出:
    最终++的结果: 9970
第n次
    ... 总之基本上没有算对的、

-------------------------------------------------------------------------------------------
而加上 synchronized 代码块后

第1次
    最终++的结果: 100002次
    最终++的结果: 10000
第三次
    最终++的结果: 10000
    
    每次都运行无误

上边 代码很简单,即:使用 多个线程成员变量n进行 10000次 ++ 操作。 我们要的结果是不管多少个线程,只要是++了一万次,那么n必须等于10000,但是如果我们不给++操作加锁,那么结果总是不尽人意,当我们给++操作加上synchronized 后,发现每次的结果很准确都是10000。 所以我们得出结论: 给有线程安全问题的代码/逻辑 加上锁后,能保证锁中逻辑是同步的写入/读取操作是安全的同一时刻,只有一个线程可以执行锁中逻辑从而避免了线程安全问题。

ps: 锁的实现有很多种,我们不做其他介绍,在这里只一心一意的说 synchronized ; 另外值得注意的是:加锁是保证线程安全的方式之一,并不是唯一方法,比如ThreadLocal 你看它哪里加锁了?人家也可以保证线程安全的,由于本文主要围绕synchronized展开,所以对其他能保证线程安全的方式 不做展开

3、synchronized使用与反汇编观察

接下来我们看下 synchronized 使用的几种 姿势

3.1、修饰成员方法

  • synchronized 修饰成员方法时,其使用的锁对象是 当前方法所在类的实例,也就是this,代码如下:
public class SyncWorkOnMemberMethodTest {
   private static int cpuCount = Runtime.getRuntime().availableProcessors();
   private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
         TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
   static {
      System.out.println("我的cpu颗数: " + cpuCount);
      threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
   }

   //成员变量,可被多个线程 共享
   private static long n = 0L;

   public static void main(String[] args) throws Exception {

      //多弄点,不然不好看出结果
      CountDownLatch countDownLatch = new CountDownLatch(10000);
      //一定要在线程外new 该对象,保证多个线程竞争的是唯一一把锁,从而达到 "互斥"
      SyncWorkOnMemberMethodTest lockObj = new SyncWorkOnMemberMethodTest();

      for (int i = 1; i < 10001; i++) {

         //注意我们不能在for循环中new 这个对象,因为那样的话锁就不是唯一的了,也就无法保证多个线程去竞争一把锁,达不到 "互斥" 的效果
         //SyncWorkOnMemberMethodTest lockObj = new SyncWorkOnMemberMethodTest();

         threadPool.execute(() -> {
            lockObj.incr(countDownLatch);
         });
      }
      //等待所有线程 执行完后再打印 ++的结果
      countDownLatch.await();
      System.out.println("最终++的结果: " + n);
   }

   //修饰成员方法,其使用的锁对象是 当前类所在的实例对象(或者说当前方法的调用对象),也就是 this
   private synchronized void incr(CountDownLatch countDownLatch) {
      try {
         n++;
      } finally {
         countDownLatch.countDown();
      }
   }
}

修饰成员方法 反汇编后得到的代码:

 //略
 
 private synchronized void incr(java.util.concurrent.CountDownLatch);
    descriptor: (Ljava/util/concurrent/CountDownLatch;)V
    flags: (0x0022) ACC_PRIVATE, ACC_SYNCHRONIZED
    Code:
      stack=4, locals=3, args_size=2
         0: getstatic     #15                 // Field n:J
         3: lconst_1
         4: ladd
         5: putstatic     #15                 // Field n:J
         8: aload_1
         9: invokevirtual #19                 // Method java/util/concurrent/CountDownLatch.countDown:()V
        12: goto          22
        15: astore_2
        16: aload_1
        17: invokevirtual #19                 // Method java/util/concurrent/CountDownLatch.countDown:()V
        20: aload_2
        21: athrow
        22: return
        
 //略

可以看到 被synchronized修饰的成员方法,有一个 ACC_SYNCHRONIZED 标记。这里我们先记住,后续分析原理时,我们再深入说。

3.2、修饰静态方法

  • synchronized 修饰静态方法时,使用的锁对象是当前类,代码如下:
public class SyncWorkOnStaticsMethodTest {
   private static int cpuCount = Runtime.getRuntime().availableProcessors();
   private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
         TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
   static {
      System.out.println("我的cpu颗数: " + cpuCount);
      threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
   }

   //成员变量,可被多个线程 共享
   private static long n = 0L;

   public static void main(String[] args) throws Exception {

      //多弄点,不然不好看出结果
      CountDownLatch countDownLatch = new CountDownLatch(10000);

      for (int i = 1; i < 10001; i++) {
         threadPool.execute(() -> {
            incr(countDownLatch);
         });
      }
      //等待所有线程 执行完后再打印 ++的结果
      countDownLatch.await();
      System.out.println("最终++的结果: " + n);
   }

   //修饰静态方法,使用的锁对象是当前类
   private static synchronized void incr(CountDownLatch countDownLatch) {
      try {
         n++;
      } finally {
         countDownLatch.countDown();
      }
   }
   
}

修饰静态方法 反汇编得到的代码:

//略

private static synchronized void incr(java.util.concurrent.CountDownLatch);
    descriptor: (Ljava/util/concurrent/CountDownLatch;)V
    flags: (0x002a) ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=4, locals=2, args_size=1
         0: getstatic     #13                 // Field n:J
         3: lconst_1
         4: ladd
         5: putstatic     #13                 // Field n:J
         8: aload_0
         9: invokevirtual #17                 // Method java/util/concurrent/CountDownLatch.countDown:()V
        12: goto          22
        15: astore_1
        16: aload_0
        17: invokevirtual #17                 // Method java/util/concurrent/CountDownLatch.countDown:()V
        20: aload_1
        21: athrow
        22: return

//略
      

可以看到 被synchronized修饰的静态方法,也有一个 ACC_SYNCHRONIZED 标记。这里我们先记住,后续分析原理时,我们再深入说。

3.3、修饰代码块

  • 修饰代码块时候,使用的锁对象是给定的 类/对象 ,代码如下:
public class SyncWorkOnObjTest {
   private static int cpuCount = Runtime.getRuntime().availableProcessors();
   private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(cpuCount, cpuCount * 2, 20,
         TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000));
   static {
      System.out.println("我的cpu颗数: " + cpuCount);
      threadPool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("测试++操作" + "-task-%d").build());
   }

   //成员变量,可被多个线程 共享
   private static long n = 0L;

   public static void main(String[] args) throws Exception {
   
      //多弄点,不然不好看出结果
      CountDownLatch countDownLatch = new CountDownLatch(10000);

      SyncWorkOnObjTest syncWorkOnObjTest = new SyncWorkOnObjTest();
      for (int i = 1; i < 10001; i++) {
         threadPool.execute(() -> {
            syncWorkOnObjTest.incr(countDownLatch);
         });
      }
      //等待所有线程 执行完后再打印 ++的结果
      countDownLatch.await();
      System.out.println("最终++的结果: " + n);
   }

   //在方法中,构成了 synchronized块的, 使用的锁对象是括号中的 类/对象
   private  void incr(CountDownLatch countDownLatch) {
      try {
         synchronized (countDownLatch){
            n++;
         }
      } finally {
         countDownLatch.countDown();
      }
   }
}

修饰代码块 反汇编后得到的代码:

  • 注意,由于这里需要看懂部分汇编指令,所以我们在关键部分加上注释,好知道这个汇编指令是干了什么。
//略

 private void incr(java.util.concurrent.CountDownLatch);
    descriptor: (Ljava/util/concurrent/CountDownLatch;)V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=4, locals=5, args_size=2
         0: aload_1
         1: dup
         2: astore_2
         3: monitorenter            //获取锁
         4: getstatic     #15       //从类中获取静态字段          // Field n:J  
         7: lconst_1                //将long类型常量1压入栈
         8: ladd                    //执行long类型的加法 
         9: putstatic     #15       //设置类中静态字段的值          // Field n:J
        12: aload_2                 
        13: monitorexit             //释放锁
        14: goto          22        
        17: astore_3
        18: aload_2
        19: monitorexit             //确保在发生异常时候,也释放monitor 锁
        20: aload_3
        21: athrow
        22: aload_1
        23: invokevirtual #19       //调用对象的实例方法:invokevirtual          // Method java/util/concurrent/CountDownLatch.countDown:()V
        26: goto          38      
        29: astore        4        
        31: aload_1
        32: invokevirtual #19                 // Method java/util/concurrent/CountDownLatch.countDown:()V
        35: aload         4
        37: athrow
        38: return                //从方法中返回,返回值为void

//略

可以看到被synchroized修饰的代码块, 在进行n++之前,会先执行 monitorenter汇编指令来获取锁,然后是get put (也就是++操作)然后是 monitorexit指令,此时,锁被当前持有的线程释放

值的注意的是: 当锁是某个对象实例时,一定要保证,该实例的唯一性,否则多个线程去竞争n个锁,是无法保证“互斥” 的特性的,只有锁对象唯一(也就是只new了一次,或者是一个class)才能多个线程去抢同一把锁,从而达到“互斥”的目的

4、到底什么是真正的锁资源?

我们在第三节给出了syncroized的使用方式以及反汇编后得到的内容分析,这里再次回顾下:

1.`synchronized`修饰的**成员方法**,有一个 `ACC_SYNCHRONIZED` 标记。
2.`synchronized`修饰的**静态方法**,也有一个 `ACC_SYNCHRONIZED` 标记。
3.`synchroized`修饰的**代码块**, 再进行代码块里的逻辑 之前,会先执行 `monitorenter`汇编指令来
`获取锁`,执行逻辑完毕后,紧接着是 `monitorexit`指令,此时,锁被当前持有的线程`释放`

那么,给方法打上 ACC_SYNCHRONIZED标记,以及执行monitorentermonitorexit指令,底层到底会做哪些操作呢?我们接着看~~

关于monitorenter指令的处理,在github.com/JetBrains/j… 文件中里边可以看到:

  • 如果开启偏向锁,就执行 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);(偏向锁)
  • 否则就执行 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);(轻量级锁) image.png

但是,注意: 在第4节,我们其实主要是在讲ObjectMonitor,而这个东西是在锁膨胀为 重量级锁时候才会使用到的,所以在下边的 4.1、 4.2、 4.3 小节中,我们都假定 锁已经膨胀为重量级。这一点要特别清楚。(同时应该知道的是:在jdk1.5(包含)之前,synchroized只有一种情况就是直接使用重量级锁,在之后的版本中,才有偏向,轻量级 这两个锁级别)

4.1、真正的资源:ObjectMonitor

实际上不管是被标记为 ACC_SYNCHRONIZED 的同步方法 还是 被插入monitorenter/monitorexit指令的同步块 ,最终JVM的底层都是一个逻辑,那就是:

  • 在进入该方法或同步块时,必须 竞争到 给定对象 对应的ObjectMonitor上的资源, (这里的 "竞争" 具体表现为:某线程通过CAS操作来将给定锁对象对应的ObjectMonitor 上的 owner指针 指向自己,CAS(具体怎么cas下边会说)操作成功,就代表获取锁成功,CAS失败,就代表未获取到锁)。 同时,我们要必须清楚的一点是,每一个对象 都有一个ObjectMonitor与之相对应,且唯一。这里给出 官方 对monitorenter的解释 来源点这里

    每个对象都与一个监视器相关联。 当且仅当监视器与某个对象相关联时,它才会被锁定。 执行 monitorenter 的线程尝试获得与 对象 关联的监视器的所有权,如下所示:

    • 如果与 对象 关联的监视器的条目(count值)计数为零(说实话我在源码中找半天也没看见count++的代码),则线程进入监视器并将其条目(count)计数设置为一。 线程就是监视器的所有者。
    • 如果线程已经拥有与 对象 关联的监视器,它会重新进入监视器,增加其count计数。
    • 如果另一个线程已经拥有与 锁对象 关联的监视器,线程将阻塞直到监视器的count计数为零,然后再次尝试获得所有权。
    • (个人补充:)我觉得最重要的一点: 抢锁的操作对应到JVM的底层来说其实就是CAS设置owner指针指向当前抢锁线程的操作,设置成功即抢锁成功,设置失败即抢锁失败。

紧接着我们看下hotspot中的ObjectMonitor 长啥样:

下边是 hotspot源码 /src/hotspot/share/runtime/objectMonitor.hpp 类中的部分片段,该片段定义了objectMonitor类的一些组成,其中有几个比较重要的成员我们会介绍一下,以便下边更好理解

// initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {¸
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

对主要字段作下说明 :

_owner: 指向当前持有锁的Thread。所谓的加锁就是抢锁线程通过CASObjectMonitor的Owner指向自己,ObjectMonitor中的Owner指向谁谁就持有锁,为null时就是空闲状态。

_WaitSet: 用来存放那些因调用了wait()方法而被阻塞到当前Monitor的线程。以便于调用notify时进行唤醒操作。这也是为什么同步代码块一旦调用wait/notify就会直接进入重量级锁模式(因为使用到了ObjectMonitor就意味着锁已经升级到重量级),以及wait/notify需要在同步代码块内执行,否则会抛出IllegalMonitorStateException的原因。

_cxq: 存放那些获取锁失败后进入阻塞状态等待锁资源释放的线程。通过CAS操作放在cxq的首部。

EntryList: 用于记录阻塞在当前Monitor上的线程

_recursions: 锁的重入次数

_count: 获取锁成功后会+1

是不是很蒙蔽?cxq和Entry List作用好像一样呀,都是存放等着抢锁的线程,其实是有点区别的,我们简单描述:cxq是预备区,优先级更低 Entry List是排队区,优先级更高些,关于为啥这么设计,欢迎评论区留言~~

4.2、锁对象与ObjectMonitor的关系

我自己对 对象和ObjectMonitor 关系的理解:

  1. 执行 monitorenter 的线程试图获得与指定对象(synchronized块中的锁对象) 关联的ObjectMonitor的所有权,每个对象都有一个监视器(ObjectMonitor)与之相关联。
  2. 当且仅当监视器(ObjectMonitor)和某个对象产生关联时(也就是说该对象的markword中指向互斥量monitor的指针: ptr_to_heavyweight_monitor 指向某个ObjectMonitor时,(或者从线程角度来说)ObjectMonitor中的owner指针指向当前线程id时候),ObjectMonitor对象标识为已被锁定/持有。
  3. 基于上边这两条理解,我画了一张图 (这里我们假设的场景是已经升级为重量级锁,如果不是重量级锁,也不存在ptr_to_lock_record指针,更不会用到ObjectMonitor,这一点我们要非常清楚) ,这里给出来使得脑海中更清楚这个关系,如下:image.png
  4. 从上边(1、2、3) 不难得出 结论(这里我们只讨论重量级锁的情况):

    所谓的锁资源(或者叫互斥量),其实并不是某个对象,而是与锁对象对应的 ObjectMonitor,多线程之间抢的资源,其实是 给定锁对象对应的ObjectMonitor中的owner 当owner指向哪个线程,哪个线程就持有锁! 抢不到锁的统统靠边站!

下边我们通过源码来看看ObjectMonitor到底做了什么~~

4.3、ObjectMonitor图解

我们在上边说了,线程真正抢的资源是ObjectMonitor,那么是怎么抢的呢?抢成功了的线程怎么处理?抢失败了的线程怎么处理?

一图胜千言,先上图,再解释。ObjectMonitor流转图如下:

image.png

对上图做个解释:

  1. 尝试获取: 当线程尝试获取锁时,会进行cas操作尝试将owner指针 指向当前线程,如果获取锁成功,则进入同步块执行逻辑
  2. 获取失败后: 从cxq队首插入,包装了当前线程的node
  3. 当持有锁的线程释放锁后: 首先肯定的就是他会将owner置位null,好让出资源,然后会从EntryList(如果没有从cxq)队列中挑选一个线程进行抢锁,被选中的线程叫做Heir presumptive即假定继承人也叫(OnDeck),假定继承人(OnDeck)尝试获得锁,但synchronized是非公平的,所以假定继承人(OnDeck)不一定能获得锁(这也是它叫"假定"继承人的原因)。
  4. 当持有锁的线程调用Object的wait方法后: 则会将当前线程加入到WaitSet队列中,
  5. 当被Object#notify/notifyAll方法唤醒后: 会将对应线程从WaitSet移动到cxq或EntryList中去(具体移动策略有点复杂,我们不再去探究)。需要注意的是: 当调用一个锁对象的wait或notify/notityAll方法时,如果当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁!(这个原因留在评论区讨论)

5、锁升级分析

由于jdk5(不含)之后对synchroized做了优化,不再是上来就加重量级锁,而是变成: 偏向->轻量级锁->重量级锁 这三种锁级别,如果再加上无锁状态 那一共就是4种,如下:

  • 无锁状态-> 偏向锁-> 轻量级锁-> 重量级锁; (但是锁的升级一般是单向的,也就是说只能从低到高升级,不会出现锁的降级(但是高版本似乎也可以降,这一点我们暂时不去关注))

下面我们看下hotspot官方给出的锁升级示意图如下:openjdk的资料点这里; 图中部分英文翻译:(inflate: 膨胀;biased: 偏向;revoke bias: 撤销偏向) image.png

上边这张图片讲的很清楚了(但是必须知道对象头相关的知识才可以看懂这张图,对象头 知识请参见我的另一篇文章:对象组成与Java内存模型JMM分析

我们对图中做个简单分析:

  1. 如果开启了偏向锁,那么升级流程大概是:偏向->轻量级->重量级
  2. 如果没开启偏向,那么升级流程大概是:轻量级->重量级
  3. 偏向锁记录的是线程id,轻量级锁记录的是指向线程栈的指针,重量级锁指向的是 ObjectMonitor

好了知道大概流程后,我们一级一级来分析一下,到底是怎么膨胀/撤销的

5.1、获取偏向锁

关于monitorenter指令的处理我们在上边提到过,这里再次简单过一下:

//------------------------------------------------------------------------------------------------------------------------
// Synchronization
//
// The interpreter's synchronization code is factored out so that it can
// be shared by method invocation and synchronized blocks.
//%note synchronization_3

//%note monitor_1
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");
  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

可以看到

  • 开启了偏向锁,那么就执行
    • ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  • 否则执行:
    • ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);

那我们就看看 ObjectSynchronizer::fast_enter 到底是干了什么吧?

// -----------------------------------------------------------------------------
//  Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 //判断是否开启了偏向锁开关
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      //尝试获取偏向锁
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      //如果是撤销与重偏向 直接返回
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }

 //走到这里,代表偏向锁未开启,则进入slow_enter获取轻量级锁的流程
 slow_enter (obj, lock, THREAD) ;
}

代码似乎很简单,尝试获取偏向锁这个我们就不做过多探究了,但我猜测里边做的事情大概如下:

  • 判断: 各种加锁前提条件判断
  • 成功: 偏向成功后,将偏向线程id偏向次数以及偏向标志记录到锁对象对象的markword中去。
  • 失败: 如果执行偏向操作失败,表示当前存在多个线程竞争锁,当达到全局安全点(safepoint,也即有其他线程参与竞争时),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级,升级完成后被阻塞在安全点的线程继续执行同步代码块。

获取偏向锁的代码演示: image.png

可以看到,在vm配置为开启偏向锁时候(也就是去掉-XX:-UseBiasedLocking参数),就算刚new出来的对象也是标记为偏向锁的,只是线程id(markword前54位全是0),在进入synchroized后,线程id有值,并还是偏向状态,因为此刻只有main线程一个竞争锁

5.2、撤销偏向锁

当到达一个全局安全点时(这里我浅显的理解为没有线程竞争时,实际上安全点的东西很复杂,写一篇万字长文讲清楚都算不错了),这时会根据偏向锁的状态来判断是否需要撤销偏向锁,调用 revoke_at_safepoint方法,这个方法是在 biasedLocking.cpp中定义的,具体实现如下:

void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
	//断言:必须在全局安全点(这里我浅显的理解为没有线程竞争时,实际上安全点的东西很复杂,写一篇万字长文讲清楚都算不错了)
  assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
  oop obj = h_obj();
  //更新撤销/偏向 计数,并返回偏向锁撤销次数和偏向次数
  HeuristicsResult heuristics = update_heuristics(obj, false);
  if (heuristics == HR_SINGLE_REVOKE) {
    //撤销偏向锁
    revoke_bias(obj, false, false, NULL);
  } else if ((heuristics == HR_BULK_REBIAS) ||
             (heuristics == HR_BULK_REVOKE)) {
    //批量撤销
    bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
  }
  //清楚缓存
  clean_up_cached_monitor_info();
}

关于断言处提到的安全点这里我们不做过多展开,大概明白这个方法所做的事情大概就是:

  • 更新撤销&偏向次数
  • 执行撤销/批量撤销
  • 由于我们之前讲过对象组成与Java内存模型JMM分析,所以可以肯定的是,撤销偏向时,会删除偏向线程id减掉偏向次数将偏向标志从1改为0

我们上边说过,当发生线程竞争时,将撤销偏向锁,膨胀为轻量级锁(或者关闭偏向锁时候,发生竞争时直接就进入轻量级锁),下边看看源码:

5.3、获取轻量级锁

在上边的InterpreterRuntime::monitorenter代码中,我们看到不开启偏向锁的话,是进入到了ObjectSynchronizer::slow_enter,此中的逻辑就是轻量级锁的获取逻辑。我们看下源码:

// -----------------------------------------------------------------------------
// Interpreter/Compiler Slow Case
// This routine is used to handle interpreter/compiler slow case
// We don't need to use fast path here, because it must have been
// failed in the interpreter/compiler code.
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  //如果是无锁
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    
    //copy markword到 当前线程的 线程栈中的Lock Record  的displaced 属性上去
    lock->set_displaced_header(mark);
    //通过CAS尝试将markword更新为指向BasicLock对象的指针,实际上这一步就是设置Lock Record指针的过程,
    //该指针指向 当前执行方法的线程栈
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } 
  //如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要抢锁 
  else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }
#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif
  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  //代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过inflate操作进行膨胀为重量级锁
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

获取轻量级锁 的逻辑大概分为三步,如下:

  1. 如果当前对象处于无锁状态(即mark->is_neutral() 成立),则进入步骤(2和3),否则执行步骤(4)

  2. 某个(私有的)线程栈上开辟LockRecord空间,并copy 锁对象对应的 markword到Lock record的_displaced_header属性中去,下边图解一下:

    • 在加锁前,hotspot 需要在当前线程的栈帧中开辟锁记录(Lock Record)的空间。 Lock Record 中包含一个 _displaced_header 属性,用于存储锁对象的 Mark Word 的副本。image.png
    • 将锁对象的markword复制到锁记录 lock record中,这个复制过来的记录叫做 Displaced Mark Word。具体来讲,是将 mark word 放到锁记录(lock record)的 _displaced_header 属性中(注意: 此时的markword是无锁状态,如果是偏向锁升级到轻量级那么会先撤销偏向,所以displaced_header是无锁的markword! 这个 displaced_header 在轻量级锁释放时候会用到 )。如图:image.png
  3. 通过CAS尝试将markword的ptr_to_lock_record 更新为 当前线程栈的地址(更准确的来说是当前线程栈中的Lock record的地址,为了简单点,我们就叫他为线程栈地址),如果更新成功(同时我们应该清楚的是,抢锁成功后肯定有一个更新最后两位为 00的操作,因为最后两位是00 才代表是轻量级锁),表示竞争到锁,则执行同步代码

  4. 如果当前markword处于加锁状态,且markword中的ptr_to_lock_record指针当前线程的栈帧此时代表重入锁,则执行同步代码,

  5. 否则 说明有多个线程竞争轻量级锁,轻量级锁进行膨胀升级 变为重量级锁

轻量级锁抢锁举例: 假设某一时刻线程A和B同时发现锁对象是无锁状态 :

  1. 此刻 线程A和B都把markword(肯定是同一个markword因为锁对象是同一个)复制到各自线程的Lock Record中的 _displaced_header字段,_displaced_header数据保存在线程的栈帧上,是线程私有的
  2. Atomic::cmpxchg_ptr原子操作保证只有一个线程可以把当前线程栈中的Lock Record地址复制到markword的ptr_to_lock_record指针上去,假设此时线程A执行成功(更新markword最后两位为00 ),则A执行同步代码块
  3. 此时线程B执行失败,则退出临界区,通过ObjectSynchronizer::inflate方法开始膨胀锁,膨胀成功后变为重量级锁

轻量级锁加锁演示(在这里我们关闭了偏向锁,如果模拟偏向锁到轻量级锁有点不太好模拟,所以我们这里直接关闭偏向锁) image.png

可以看到在关闭偏向锁后,进入同步块的话直接就使用轻量级锁了 可以看到后三位为 0 00 标识此时是轻量级锁状态。

5.4、撤销轻量级锁

与偏向锁不同(偏向锁的撤销是达一个全局安全点时,或者遇到锁竞争,需要升级为轻量级时候),而轻量级锁的释放,需要执行到monitorexit指令(也就是说执行完同步块的时候)

//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (elem == NULL || h_obj()->is_unlocked()) {
    THROW(vmSymbols::java_lang_IllegalMonitorStateException());
  }
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  // Free entry. This must be done here, since a pending exception might be installed on
  // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
  elem->set_obj(NULL);
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END


上边的slow_exit是释放锁的主逻辑,而slow_exit中又调了ObjectSynchronizer::fast_exit
所以我们直接看fast_exit这个逻辑如下:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
  // if displaced header is null, the previous enter is recursive enter, no-op
  //获取 线程栈帧中的锁记录(LockRecord)中的 displaced_header中的 markword(在加锁时候copy的锁对象中的markword)
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {
     // Recursive stack-lock.
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     mark = object->mark() ;
     assert (!mark->is_neutral(), "invariant") ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
        assert(((oop)(m->object()))->mark() == mark, "invariant") ;
        assert(m->is_entered(THREAD), "invariant") ;
     }
     return ;
  }
  //获取锁对象中的对象头 
  mark = object->mark() ;

  // If the object is stack-locked by the current thread, try to
  // swing the displaced header from the box back to the mark.
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     //通过CAS尝试把当前锁对象Mark Word替换为 dhw的mark word
     //(这里的dhw是加锁时候copy的锁对像的 那时候是无锁状态 ,后三位是 0 01 )这一点一定要清楚
     //替换成功后,也就代表当前锁对对象的markword的后三位是 0 01 即无锁状态,代表释放锁成功
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  //如果CAS失败,说明有其它线程在尝试获取该锁,这时需要将该锁升级为重量级锁并释放
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

以上轻量级锁的释放,和那个displaced_header有很大关系,我们一定要清楚displaced_header是干啥的,不明白的看看轻量级锁加锁时候 displaced_header是放的啥。

以上 轻量级锁的释放 大概是以下几步:

  1. 取出在轻量级锁加锁时保存在 当前持有锁的线程栈 中的Lock Record对象上displaced_header属性中的markword数据 然后赋值给 dhw变量 。(我去,一层套一层真的是哈哈哈~)
    • 值得注意的是 因为在轻量级锁加锁时候,copy markword到线程栈的Lock record的displaced_header中时候,是无锁状态(如果是偏向锁升级到轻量级锁的,会先撤销偏向),也就是当时markword后三位是 0 01,所以在第三步,释放轻量级锁时候,如果dhw替换锁对象 的markword成功,也就意味着释放成功了,这个我们应该要清楚。
  2. 通过CAS尝试把dhw替换到当前的Mark Word,如果CAS成功,说明成功的释放了锁,否则执行步骤(3);
  3. 如果CAS失败,说明有其它线程在尝试获取该锁,这时需要将该锁升级为重量级锁,并释放;

5.5、锁膨胀

锁膨胀 我们这里不做源码展开了,简单说下膨胀时候干的几件事情:

  1. 判断当前是否为重量级锁,即Mark Word的锁标识位为 10,如果当前状态为重量级锁,执行步骤(2),否则执行步骤(3);
  2. 获取指向ObjectMonitor的指针,并返回,说明膨胀过程已经完成;
  3. 如果当前锁处于膨胀中,说明该锁正在被其它线程执行膨胀操作,则当前线程就进行自旋等待锁膨胀完成,这里需要注意一点(虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起);如果其他线程完成锁的膨胀操作,则退出自旋并返回;
  4. 如果当前是轻量级锁状态,即锁标识位为 00,膨胀过程如下:
    • 通过omAlloc方法,获取一个可用的ObjectMonitor monitor,并重置monitor数据;
    • 通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,表示当前锁正在膨胀中,如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成; 如果CAS成功,设置monitor的各个字段:_header、_owner和_object等,并返回;

锁膨胀整体总结:

锁膨胀的过程实际上是获得一个与锁对象对应的ObjectMonitor对象监视器(一个锁对象与一个ObjectMonitor一一对应),而真正抢占锁的逻辑,在 ObjectMonitor::enter方法里面(我们紧接着就要讲到)

5.6、获取重量级锁

接下来我们看下hotspot的代码,来看下主流程: 我们看下里边的主要抢锁逻辑对应函数为 -> ObjectMonitor::enter:

void ATTR ObjectMonitor::enter(TRAPS) {
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD ;
  void * cur ;
  //************: cas操作,将Owner指针 尝试指向给定的线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  // ************: cur == null 代表获取到锁,直接返回
  if (cur == NULL) {
     // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }
  //************: 获取到锁的是自己 将重入计数器 +1
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
  //************: 当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀且第一次调用enter方法,那cur是指向Lock Record的指针
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    // Commute owner from a thread-specific on-stack BasicLockObject address to
    // a full-fledged "Thread *".
    _owner = Self ;//设置owner字段为当前线程(之前owner是指向Lock Record的指针)
    OwnerIsThread = 1 ;
    return ;
  }
  // We've encountered genuine contention.//遇到了锁资源的竞争
  assert (Self->_Stalled == 0, "invariant") ;
  Self->_Stalled = intptr_t(this) ;
  // Try one round of spinning *before* enqueueing Self
  // and before going through the awkward and expensive state
  // transitions.  The following spin is strictly optional ...
  // Note that if we acquire the monitor from an initial spin
  // we forgo posting JVMTI events and firing DTRACE probes.
  //在调用系统的同步操作之前,先尝试自旋获得锁
  if (Knob_SpinEarly && TrySpin (Self) > 0) {
     assert (_owner == Self      , "invariant") ;
     assert (_recursions == 0    , "invariant") ;
     assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
     //自旋的过程中获得了锁,则直接返回
     Self->_Stalled = 0 ;
     return ;
  }
略 ...
    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    for (;;) {
      jt->set_suspend_equivalent();
      // cleared by handle_special_suspend_equivalent_condition()
      // or java_suspend_self()
      //到这里还没获取到锁,则进行系统调用挂起当前线程(但是这个EnterI也不是直接挂起,
      //在插入cxq队列后,他还会尝试获取锁,可以看出是多么的努力~~~)
      EnterI (THREAD) ;
      ...
    }
    Self->set_current_pending_monitor(NULL);
  }

抢锁流程总结: 从上边源码可以得到信息,即当一个线程尝试获得锁时的流程大致如下:

  1. 如果cas成功,那么就直接返回了。
  2. 如果 cur == Self 即当前占有锁的线程是自己,那么就会将 _recursions重入次数加一(多说一句:重入了几次,在释放锁时候就得释放几次,直到次数为0才代表完全释放掉资源)。
  3. 经过各种尝试后(多次自旋),如果其中有一次成功则返回抢锁成功,如果多次自旋后该锁确实已经被占用,进入到下一步。
  4. 进入到EnterI 逻辑,其中EnterI 逻辑比较复杂,我们不再贴代码了要不代码占用太多,直接来描述下里边的主要逻辑,如下:
    • 进入EnterI后并不是立马将当前线程挂起,而是先放入cxq队列,然后再次尝试获取锁,如果还没获取到锁,才将当前线程挂起也就是调用park函数(对于park函数里边的逻辑我们就不展开了,有兴趣可以google下),对应系统调用则是linuxpthread_cond_wait方法 (作用是挂起线程也可以支持指定时长的挂起),我们截图证实一下, hotsport源码点这里 image.png image.png

代码演示下重量级锁的情况,如下: image.png

可以看到,由于我们在线程池中循环提交了10个任务,也就是说有10个线程在抢锁(竞争相当激烈),所以锁膨胀为重量级 后两位 是 10 代表重量级锁,而后边的 62位是 00000000 00000000 01111111 10011000 01010101 01100001 00111110 000000,这一串其实就是markword中ptr_to_heavyweight_monitor的值! 。

到此,抢锁逻辑就差不多了,有抢锁就有释放锁,下边我们看下释放锁的逻辑~~

5.7、释放重量级锁

当抢到锁的线程执行完同步块的逻辑后,会碰到 monitorexit指令,执行这个指令,线程就会释放锁(值的一提的是如果monitorexit失败,那么就会在finally中再次monitorexit,确保锁一定释放,避免死锁,这个小细节在我们的第三节-> synchroized的反汇编中可以观察的到),从而让出资源,供其他线程获取。

注意:(如果是标记为 ACC_SYNCHRONIZED的同步方法,那么会在方法结束后进行锁释放)

总而言之,不管是同步方法还是同步代码块,都会释放锁,而他们释放锁的hotspot底层对应的都是同一处代码,如下:

void ObjectMonitor::exit(JavaThread* current, bool not_suspended) {
  void* cur = owner_raw();
  //如果_owner不是当前线程并且当前线程是之前持有轻量级锁的线程 (因为轻量级锁膨胀后还没调用过enter方法),则将_owner指向Lock Record地址。
  if (current != cur) {
    if (current->is_lock_owned((address)cur)) {
      assert(_recursions == 0, "invariant");
      set_owner_from_BasicLock(cur, current);  // Convert from BasicLock* to Thread*.
      _recursions = 0;
    } 
    
      //略 一大堆
      
  //如果重入次数不等于0 则执行 --  操作并返回,这里返回后会重新进入exit,也即线程a加了n次锁那么解锁时候就--n次
  if (_recursions != 0) {
    _recursions--;        // this is simple recursive enter
    return;
  }

  for (;;) {
    assert(current == owner_raw(), "invariant");

    // Drop the lock.
    // release semantics: prior loads and stores from within the critical section
    // must not float (reorder) past the following store that drops the lock.
    // Uses a storeload to separate release_store(owner) from the
    // successor check. The try_set_owner() below uses cmpxchg() so
    // we get the fence down there.
    // 删除锁 从方法名上可以看到 是clear owner ,我猜测最主要的就是通过cas 将owner置为null
    release_clear_owner(current);
    OrderAccess::storeload();//插入内存屏障 ,保证上边的 release_clear_owner结果对其他正在自旋的线程可见
    //(意为:其他正在自旋的线程可以竞争锁了,注意这里抢锁的 可不是在EntryList中的阻塞线程,因为此时还没有唤醒EntryList/cxq  
    //中的阻塞线程呢得到下边 的 ExitEpilog方法中才是唤醒操作 )

    if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != nullptr) {
      return;
    }
    // Other threads are blocked trying to acquire the lock.
    //到这一步 代表 其他已经阻塞的线程,可以尝试获取锁资源了
    
    //省略 一堆注释,感兴趣可以看看源码中的注释
    
    //由于上边释放了锁,所以在释放锁到此刻如果有线程(通过自旋)抢到的话,当前线程需要抢锁,以便执行下边的唤醒逻辑
    if (try_set_owner_from(nullptr, current) != nullptr) {
    //没获取到锁(代表在这么一刹那有线程通过自旋获取到锁了) 即 结果 != nullptr 则返回 进行下一次循环
    //(即等占用该锁的线程释放后再处理链表 cxq/EntryList 中的等待(blocked)的线程)
      return;
    }

    guarantee(owner_raw() == current, "invariant");
    ObjectWaiter* w = nullptr;
    //检查EntryList(可以=看出 EntryList比cxq优先级要高) 如果队列中不是空 ,则执行 ExitEpilog逻辑
    //(即唤醒等待的线程)
    w = _EntryList;
    if (w != nullptr) {
      ExitEpilog(current, w);
      return;
    }

    // If we find that both _cxq and EntryList are null then just
    // re-run the exit protocol from the top.
    //如果 cxq和 _EntryList队列都是空,则执行 continue
    w = _cxq;
    if (w == nullptr) continue;
    
    // Drain _cxq into EntryList - bulk transfer.
    // First, detach _cxq.
    // The following loop is tantamount to: w = swap(&cxq, nullptr)
    //走到这里 代表 _EntryList是空 并且 _cxq不是空,则搬运 cxq元素到  _EntryList队列中去(这也印证了我们说的 _EntryList是排队区,cxq是等待区的说法)。
    for (;;) {
    //通过cas操作 循环取出 cxq队列的元素 放到w变量中去(后续的唤醒都是从w中取的元素) ,当cxq为空时候跳出循环 
      assert(w != nullptr, "Invariant");
      ObjectWaiter* u = Atomic::cmpxchg(&_cxq, w, (ObjectWaiter*)nullptr);
      if (u == w) break;
      w = u;
    }

    assert(w != nullptr, "invariant");
    assert(_EntryList == nullptr, "invariant");

    // 将 w 赋值给  _EntryList 
    _EntryList = w;
    ObjectWaiter* q = nullptr;
    ObjectWaiter* p;
    for (p = w; p != nullptr; p = p->_next) {
      guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->_prev = q;
      q = p;
    }

    //(这个注释很重要,我们在ExitEpilog中会详细标注) In 1-0 mode we need: ST EntryList; MEMBAR #storestore; ST _owner = nullptr
    // The MEMBAR is satisfied by the release_store() operation in ExitEpilog().

    // See if we can abdicate to a spinner instead of waking a thread.
    // A primary goal of the implementation is to reduce the
    // context-switch rate.
    if (_succ != nullptr) continue;

    w = _EntryList;//将_EntryList 赋值给 w  
    //w 不是空 进入 ExitEpilog 执行唤醒操作
    if (w != nullptr) {
      guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
      //唤醒
      ExitEpilog(current, w);
      return;
    }
  }
}

//释放锁时候的收尾操作 ps: 比较重要的操作
void ObjectMonitor::ExitEpilog(JavaThread* current, ObjectWaiter* Wakee) {
  assert(owner_raw() == current, "invariant");

  // 退出时的几个步骤(重要!!!)
  // 1. ST _succ = wakee 唤醒线程赋值给 _succ 
  // 2. membar #loadstore|#storestore; 插入内存屏障 保证其他线程可见 
  //(这一步插入内存屏障是synchroized可以保证可见性的根本原因,关于内存屏障我们稍后会简单说一下)
  // 2. ST _owner = nullptr  将owner指针置为 null
  // 3. unpark(wakee)    调用unpark函数,通过操作系统的 pthread_cond_signal 功能(如Linux) 来真正唤醒线程!!!

  _succ = Wakee->_thread;
  ParkEvent * Trigger = Wakee->_event;

  // Hygiene -- once we've set _owner = nullptr we can't safely dereference Wakee again.
  // The thread associated with Wakee may have grabbed the lock and "Wakee" may be
  // out-of-scope (non-extant).
  Wakee  = nullptr;

  // Drop the lock.
  // Uses a fence to separate release_store(owner) from the LD in unpark().
  
  //把owner置null,因为上边释放锁后,在处理等待队列数据时,又加了一次锁,所以这里需要释放锁
  release_clear_owner(current);
  
  //这一步就是插入了  #loadstore|#storestore; 内存屏障,即保证当前线程的操作,
  //对其他线程可见,也是synchroized实现可见性的根本原因!
  OrderAccess::fence();

  DTRACE_MONITOR_PROBE(contended__exit, this, object(), current);
  //触发unpark操作 唤醒线程
  Trigger->unpark();

  // Maintain stats and report events to JVMTI
  OM_PERFDATA_OP(Parks, inc());
}

以上就是释放锁时候的核心逻辑,可以看到释放锁大概分为以下几步:

  1. 先做一些判断,然后开始释放锁,并紧跟着插入storeload类型的内存屏障 ,保证上边的 release_clear_owner(释放锁)结果对其他正在自旋的抢锁线程可见。
  2. 如果在此期间没有自旋的线程获取到锁,则重新上锁 ,否则continue 来等待其他线程释放锁
  3. 上锁成功,开始处理等待队列中的线程
    • 检查EntryList如果存在元素,则取出进行唤醒操作
    • 如果EntryList是空 并且cxq不是空,则搬运 cxq元素到 w变量
    • 将w赋值给EntryList,并标识一些状态
  4. 将EntryList赋值给w,判断w不是空,进入ExitEpilog方法,进行退出前的最后/收尾 操作
  5. ExitEpilog(个人认为比较重要的方法)中有以下几步:
      1. 赋值:
        • ST _succ = wakee 唤醒线程赋值给 _succ
      1. 插入内存屏障: membar #loadstore|#storestore
        • 这一步插入内存屏障是synchroized可以保证可见性的根本原因!!!(可见synchroized和volatile一样,可见性都是通过插入内存屏障来实现的)

        • (loadstore:保证在 写主内存 操作前 其他线程一定已经读完
        • (storestore:保证当前线程写入主内存后其他线程可见
      1. 将owner指针置位null:
        • ST _owner = nullptr: 将owner指针 置为 null,释放出锁资源
      1. 通过操作系统的pthread_cond_signal唤醒线程:
        • unpark(wakee): 调用unpark函数,通过操作系统的 ( 如Linux的pthread_cond_signal ) 来真正唤醒线程!!!,进行锁竞争。

内存屏障补充: 上边有关于内存屏障的东西,我们这里做下补充说明。

  1. LoadLoad屏障:
    • 对于这样的语句 Load1; LoadLoad; Load2,
    • LoadLoad屏障会保证: 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕(说白了: 就是如果插入了LoadLoad内存屏障,就可以保证在Load2读之前,Load1 一定读完了)。
  2. StoreStore屏障:
    • 对于这样的语句 Store1; StoreStore; Store2,
    • StoreStore屏障会保证: 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
  3. LoadStore屏障:
    • 对于这样的语句Load1; LoadStore; Store2,
    • LoadStore屏障会保证: 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  4. StoreLoad屏障:
    • 对于这样的语句Store1; StoreLoad; Load2,
    • StoreLoad屏障会保证: 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

到这里,释放锁的逻辑就结束了,流程图就不画了,我想我已经说清楚主流程了。

总结

  1. 在第二节:,我们讨论了多线程的利弊,并且通过一个小demo引出线程安全问题,然后引出了synchroized,这节比较简单。
  2. 在第三节:,我们列出了synchroized的几种使用方式,并且简单的观察了其反汇编后的内容,知道了:
    • 被synchroized修饰的成员方法/静态方法 会被打上一个 ACC_SYNCHRONIZED 标记
    • 被synchroized修饰的代码块,会在执行代码块前插入 monitorenter指令,在退出代码块时候执行monitorexit指令(如果有正常逻辑有异常,hotspot保障还会在finally中执行monitorexit,以避免发生死锁)
  3. 在第四节:,我们说了真正的互斥资源 ObjectMonitor,(在假设当前锁升级为重量锁的前提下)强调了一个锁对象,必然有一个ObjectMonitor与之一一对应,在发生锁竞争的时候,其实是抢锁线程通过cas设置自己的id(实际代码中是一个包装对象,里边含有线程信息)到ObjectMonitor的_owner指针上去
    • 同时我们还图描述了 ObjectMonitor和锁对象的关系 ,把这张图再次晒出来,更直观些: image.png
    • 另外,我们描述了ObjectMonitor的流转,包括抢锁成功/失败/入队/唤醒等状态下的示意图,如下: image.png
  4. 在第五节: (这节偏向源码分析)我们描述了锁升级的过程: 无锁(无锁本文没讲,在我的上一篇有)->偏向->轻量级->重量级 的锁获取与撤销的源码分析和代码演示(这个流程图我就偷个懒,从网上找一个吧感觉这个图还算可以,我已经黑眼圈了~~~)。 image.png

结语: 到此,本文就结束了,我想这回我对sycnhroized又有了新的认识。另外,还有很多相关的东西没有讲,我想放到后边的文章来说,例如 wait/notify、锁的各种优化(自旋锁、自适应自旋,锁消除,锁粗化)等等,这都放到后边慢慢说吧,由于时间原因,这东西很难一次说完~~~

欢迎评论,留言讨论,一起交流!


巨人的肩膀:

hotspot源码和openjdk的wiki 这两个资料文中已经有粘出来了

竹子大佬的文章: juejin.cn/post/697774…

最后的总结图是参考的这个博客:blog.csdn.net/zhoutaoping…

还有这个文章感觉也不错: www.cnblogs.com/dennyzhangd…