file

上一节你了解了什么是CAS、synchronized形成的锁的类型、重量级锁是用户态进程向内核态申请资源加锁过程,HotSpot Java对象结构,以及初步从3个层面分析了下synchronized的核心流程。还记得核心流程图么?

如下所示:

file

这一节我们仔细来分析下这个过程中,每一步的底层原理。我们需要用到一个工具包,JOL,它可以将java对象的信息打印出来。你可以通过这个工具分析升级过程中锁的标记变化。

synchronized锁升级流程详解

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> synchronized锁升级流程详解</span></h3></div>

首先是我们看一下:

  • 偏向锁未启动:无锁态 new - > 普通对象。
  • 偏向锁已启动:无锁态 new - > 匿名偏向锁。

我们来看个例子: 设置JVM参数,-XX:BiasedLockingStartupDelay=10 环境:JDK1.8

<dependency>
 <groupId>org.openjdk.jol</groupId>
 <artifactId>jol-core</artifactId>
 <version>0.10</version>
</dependency>
  public class HelloSynchronized {
    public static void main(String[] args) {
      Object object = new Object();
      System.out.println(ClassLayout.parseInstance(object).toPrintable());
      synchronized (object){
      }
    }
  }

输出结果如下:

  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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

大端还是小端序? System.out.println(ByteOrder.nativeOrder()); 可以查看当前cpu的字节序。输出是LITTLE_ENDIAN意味着是小端序 l 小端序:数据的高位字节存放在地址的高端 低位字节存放在地址低端 l 大端序: 数据的高位字节存放在地址的低端 低位字节存放在地址高端 比如一个整形0x1234567 ,1是高位数据,7是低位数据。按照小端序01放在内存地址的高位,比如放在0x100 ,23就放在0x101以此类推。大端序反之。

如下图:(图片来源于网络)

file

可以看到OFFSET为0-4的Obejct header 的Value中 0 01这个标记。

也就是说,Object o = new Object() 默认的锁 = 0 01 表示了无锁态 注意:如果偏向锁打开,默认是匿名偏向状态。

可以修改JVM参数-XX:BiasedLockingStartupDelay=0。再次运行

 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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
   12   4    (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到OFFSET为0-4的Obejct header 的Value中 1 01这个标记。表示一个偏向锁,为什么说是匿名的呢?因为在JVM底层C++代码中,偏向锁默认有一个C++变量JavaThread指针,使用54位记录这个指针,从OFFSET为0-4的Obejct header 的Value中看到除了锁的标记为是101外,其余都是0,表示没有JavaThread指针无,所以是一个匿名偏向。

偏向锁未启动是指什么? 偏向锁未启动指默认情况 偏向锁有个时延,默认是4秒(不同JDK版本可以不一样) 可以通过一个JVM参数控制,-XX:BiasedLockingStartupDelay=4。因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

所以这个2个流程的变化如下图所示:

file

接着我们看往后看:

  • 偏向锁已启动:无锁态 new - > 匿名偏向锁 - 》 偏向锁
  • 偏向锁未启动:无锁态 new - > 普通对象 - 》 偏向锁

当执行到同步代码时候,有了明确的加锁线程,所以我们增加一行日志,打印Object的对象头信息,会发现,已经发生如下变化:

  public class HelloSynchronized {
    public static void main(String[] args) {
      Object object = new Object();
      System.out.println(ClassLayout.parseInstance(object).toPrintable());
      synchronized (object){
       System.out.println(ClassLayout.parseInstance(object).toPrintable());
     }
    }
  }
 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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
   12   4    (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 

 java.lang.Object object internals:
 OFFSET SIZE  TYPE DESCRIPTION                VALUE
    0   4    (object header)              05 f8 ba 86 (00000101 11111000 10111010 10000110) (-2034567163)
    4   4    (object header)              b0 01 00 00 (10110000 00000001 00000000 00000000) (432)
    8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
   12   4    (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到OFFSET为0-4的Obejct header 的Value中 1 01这个标记之外,不在全部是0,说明已经不再是匿名偏向锁了。

如果原来不是匿名偏向锁,只是一个普通对象,进入synchronized代码块后,会直接变成偏向锁。如下图所示:

file

  • 偏向锁未启动:无锁态 new - > 普通对象 - > 轻量级锁(自旋锁)

    接下来我们看一下,无锁也有可能直接变成轻量级锁。设置JVM参数,-XX:BiasedLockingStartupDelay=10,在synchronized内部加入JOL的打印输出,就会打印如下对象信息:

    //-XX:BiasedLockingStartupDelay=10
    public static void main(String[] args) {
    Object object = new Object();
    System.out.println(ClassLayout.parseInstance(object).toPrintable()); //new-普通对象 0 01
     synchronized (object){
    System.out.println(ClassLayout.parseInstance(object).toPrintable()); //new->轻量锁 00
     }
    }
 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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


  java.lang.Object object internals:
  OFFSET SIZE  TYPE DESCRIPTION                VALUE
     0   4    (object header)              e0 f0 9f 70 (11100000 11110000 10011111 01110000) (1889530080)
     4   4    (object header)              2a 00 00 00 (00101010 00000000 00000000 00000000) (42)
     8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

此流程如下图所示:

file

  • 偏向锁->轻量级锁(轻度竞争)

当有线程竞争锁时,会撤销偏向锁,升级轻量级锁。

  //-XX:BiasedLockingStartupDelay=0
  public static void main(String[] args) {
    Object object = new Object();
    System.out.println("初始化new");
    System.out.println(ClassLayout.parseInstance(object).toPrintable()); //101+全是0 匿名偏向锁
    synchronized (object){
      System.out.println(ClassLayout.parseInstance(object).toPrintable());//101+非0 偏向锁
    }

    new Thread(()->{
 try {
   Thread.sleep(1000);
     synchronized (object){
     System.out.println("t线程获取锁");
     System.out.println(ClassLayout.parseInstance(object).toPrintable()); //00 object被另一个线程加锁,发生竞争,偏向锁->轻量锁
       }
      } catch (InterruptedException e) {}
    }).start();
  }
 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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes

  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 
  java.lang.Object object internals:
  OFFSET SIZE  TYPE DESCRIPTION                VALUE
     0   4    (object header)              05 20 cc 74 (00000101 00100000 11001100 01110100) (1959534597)
     4   4    (object header)              b3 01 00 00 (10110011 00000001 00000000 00000000) (435)
     8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  java.lang.Object object internals:
  OFFSET SIZE  TYPE DESCRIPTION                VALUE
     0   4    (object header)              d8 f3 df 89 (11011000 11110011 11011111 10001001) (-1981811752)
     4   4    (object header)              46 00 00 00 (01000110 00000000 00000000 00000000) (70)
     8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到锁的变化从匿名偏向->偏向->轻量锁。这里简单提下轻量锁的底层原理:

当变成轻量锁,如果有别的线程尝试获取锁,会在线程在自己的线程栈生成LockRecord C++对象,用CAS操作将markword中62位地址,使用引用(C++叫指针)指向自己这个线程的对应的LR对象,如果设置成功者得到锁,否则继续CAS执行循环自旋操作。(PS:轻量锁的底层是使用一个LockRecord C++对象,偏向使用的是JavaThread这个对象指针)

整个升级流程如下图所示:

file

  • 偏向锁->重量级锁(重度竞争)

很早之前JDK判断竞争加剧的条件是:有线程超过10次自旋(可以通过-XX:PreBlockSpin) 或者自旋线程数超过CPU核数的一半。但是1.6之后,加入自适应自旋 Adapative Self Spinning的机制,由JVM自己控制升级重量级锁。

升级时,向操作系统申请资源,通过linux mutex申请互斥锁 , CPU从3级到0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。

//-XX:BiasedLockingStartupDelay=0
  public static void main(String[] args) {
    System.out.println(ByteOrder.nativeOrder());
    Object object = new Object();
    System.out.println(ClassLayout.parseInstance(object).toPrintable()); //101+全是0 匿名偏向锁
    System.out.println("初始化new");

    synchronized (object){
      System.out.println(ClassLayout.parseInstance(object).toPrintable());//101+非0 偏向锁
    }

    for(int i=0;i<10;i++){
      new Thread(()->{
        try {
          Thread.sleep(1000);
          synchronized (object){
            System.out.println(Thread.currentThread().getName()+"线程获取锁");
            System.out.println(ClassLayout.parseInstance(object).toPrintable()); //10 object被多个线程竞争 ,偏向锁->重量锁
          }
        } catch (InterruptedException e) {}
      }).start();
    }
  }
 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)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


 // 初始化new
  java.lang.Object object internals:
  OFFSET SIZE  TYPE DESCRIPTION                VALUE
     0   4    (object header)              05 08 18 e4 (00000101 00001000 00011000 11100100) (-468187131)
     4   4    (object header)              1f 02 00 00 (00011111 00000010 00000000 00000000) (543)
     8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

  //Thread-0线程获取锁
  java.lang.Object object internals:
  OFFSET SIZE  TYPE DESCRIPTION                VALUE
     0   4    (object header)              02 8f 57 ff (00000010 10001111 01010111 11111111) (-11038974)
     4   4    (object header)              1f 02 00 00 (00011111 00000010 00000000 00000000) (543)
     8   4    (object header)              00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
    12   4    (loss due to the next object alignment)
  Instance size: 16 bytes
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

上面的代码可以看出,锁升级是从匿名偏向锁->偏向锁->重量锁的过程,JVM判断出for循环中创建了10个线程,竞争激烈,当线程获取锁的时候直接就是重量级锁。如下图所示:

file

最后一条线,轻量级锁到重量级锁的代码我就不演示了,当竞争加剧的时候,轻量级锁会升级为重量级锁的。

好了,到这里相信你对synchronized的锁升级流程已经理解的非常清楚了。接下来我们看一些锁升级过程中的一些原理和细节。

锁升级流程中的核心原理和细节

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">锁升级流程中的核心原理和细节</span></h3></div>

既然synchronized的锁机制和java对象头的结构密切相关,对象头中的markword有锁标记,分代年龄,指针引用等含义。接下来就让我们仔细分析下偏向锁、自旋锁、重量级锁它们的底层原理和对象头中的markword的联系。

偏向锁的基本原理

file

轻量锁的C++实现机制和可重入性(基于栈)

轻量锁的原理和偏向锁类似,只不过markWord中的指针是一个LockRecord,并且修改指针的操作为CAS,那个线程CAS设置成功就会获取锁。如下图所示:

file

synchronized的锁是可重入的,这样子类才可以调用父类的同步方法,不会出问题。使用同一个对象或者类也可以多次加synchronized的代码块。所以轻量锁重入性的实现是基于入栈LR对象,来记录重入次数的。如下所示:

file

重量锁的C++实现机制和可重入性(基于ObjectMonitor类似于AQS)

重量级锁的底层原理,是通过在Mark Word里就有一个指针,是指向了这个对象实例关联的monitor对象的地址,这个monitor是c++实现的,不是java实现的。这个monitor实际上是c++实现的一个ObjectMonitor对象,里面包含了一个_owner指针,指向了持有锁的线程。ObjectMonitor它的C++结构体如下:

  // objectMonitor.hpp
  ObjectMonitor() {
    _header    = NULL;
    _count    = 0; // 重入次数
    _waiters   = 0,
    _recursions  = 0;
    _object    = NULL;
    _owner    = NULL; // 获得锁的线程
    _WaitSet   = NULL; // 调用wait()方法被阻塞的线程 
    _WaitSetLock = 0 ;
    _Responsible = NULL 
    _succ     = NULL ;
    _cxq     = NULL ;
    FreeNext   = NULL ;
    _EntryList  = NULL ; // Contention List中那些有资格成为候选人的线程被移到Entry List
    _SpinFreq   = 0 ;
   _SpinClock  = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

  }

ObjectMonitor里还有一个entrylist,想要加锁的线程全部先进入这个entrylist等待获取机会尝试加锁,实际有机会加锁的线程,就会设置_owner指针指向自己,然后对_count计数器累加1次。

各个线程尝试竞争进行加锁,此时竞争加锁是在JDK 1.6以后优化成了基于CAS来进行加锁,理解为跟之前的Lock API的加锁机制是类似的,CAS操作,操作_count计数器,比如说将_count值尝试从0变为1。

如果成功了,那么加锁成功了count加1,修改成;如果失败了,那么加锁失败了,就会进入waitSet等待。

然后释放锁的时候,先是对_count计数器递减1,如果为0了就会设置_owner为null,不再指向自己,代表自己彻底释放锁。

如果获取锁的线程执行wait,就会将计数器递减,同时_owner设置为null,然后自己进入waitset中等待唤醒,别人获取了锁执行类似notifyAll的时候就会唤醒waitset中的线程竞争尝试获取锁。

整个过程如下所示:

file

可能你会问,那尝试加锁这个过程,也就是对_count计数器累加操作,是怎么执行的?如何保证多线程并发的原子性呢?

很简单,这个地方count操作是一个类似于CAS的操作。

其实,你如果了解ReentrantLock底层的AQS机制,你就会发现,synchronized底层的实现和AQS差不多的。

只不过synchronized的底层是ObjectMonitor,它的地位就跟ReentrantLock里的AQS对应的实现Sync组件是差不多的。之后我们讲到ReentrantLock的时候你就会发现了。

为什么有自旋锁还需要重量级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁(自旋锁)。

比如JVM启动过程,会有很多线程竞争(已经明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。

锁消除

 public void add(String str1,String str2){
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
} 

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

锁粗化

  public String test(String str){
    int i = 0;
    StringBuffer sb = new StringBuffer():
    while(i < 100){
      sb.append(str);
      i++;
    }
    return sb.toString():
 } 

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

wait和notify必须和sychronized一起使用!?

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">wait和notify必须和sychronized一起使用!?</span></h3></div>

wait和notify / notifyAll还是挺有用的,在多线程开发中和很多开源项目中。那么如何使用wait和notifyall呢?它们的作用主要是线程通信,所以某个线程可以用wait处于等待状态,其他线程可以用notify来通知它,或者说是唤醒它。

wait与notify实现的一个底层原理其实和synchronized的重量级锁原理类似,主要也是monitor对象。需要注意的是必须得对同一个对象实例进行加锁,这样的话,他们其实操作的才是通一个对象实例里的monitor相关的计数器、wait set。

换句话说,wait与notify,必须在synchronized代码块中使用。因为wait/notify底层都是C++代码,是针对ObjectMonitor进行操作的。

举个例子:

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

    Object o = new Object();

    Thread waitThread = new Thread(() -> {
      try {
        synchronized (o) {
          System.out.println(Thread.currentThread().getName() + "线程获取锁,进行wait操作");
          o.wait();
          System.out.println(Thread.currentThread().getName() + "线程继续执行,之后释放了锁");

        }
      } catch (InterruptedException e) {

      }

    });

        
    waitThread.start();
 

    Thread notifyThread =new Thread(()->{

      try {
        Thread.sleep(2000);
        synchronized (o){
          System.out.println(Thread.currentThread().getName()+"线程获取锁,执行notify唤醒操作");
          o.notify();
          System.out.println(Thread.currentThread().getName()+"线程继续执行,之后释放了锁");
        }

      } catch (InterruptedException e) {}

    });

    notifyThread.start();

  }

上面代码的流程如下图所示:

file

上面过程涉及很多细节,需要仔细研究HotSpot C++代码,有兴趣的同学可以研究下wait和notify/notifyAll的C++代码。

大多情况下,核心还是掌握ObjectMonitor这个实现机制原理即可。你可能还有一些疑问,我找了一些wait和notify相关的常见的问题,供大家参考。

(以下转载自:https://zhuanlan.zhihu.com/p/...)。

为何要加synchronized锁

从实现上来说,这个锁至关重要,正因为这把锁,才能让整个wait/notify玩转起来,当然我觉得其实通过其他的方式也可以实现类似的机制,不过hotspot至少是完全依赖这把锁来实现wait/notify的。

wait方法执行后未退出同步块,其他线程如何进入同步块

这个问题其实要回答很简单,因为在wait处理过程中会临时释放同步锁,不过需要注意的是当某个线程调用notify唤起了这个线程的时候,在wait方法退出之前会重新获取这把锁,只有获取了这把锁才会继续执行,想象一下,我们知道wait的方法是被monitorenter和monitorexit的指令包围起来,当我们在执行wait方法过程中如果释放了锁,出来的时候又不拿锁,那在执行到monitorexit指令的时候会发生什么?当然这可以做兼容,不过这实现起来还是很奇怪的。

为什么wait方法可能抛出nterruptedException异常

这个异常大家应该都知道,当我们调用了某个线程的interrupt方法时,对应的线程会抛出这个异常,wait方法也不希望破坏这种规则,因此就算当前线程因为wait一直在阻塞,当某个线程希望它起来继续执行的时候,它还是得从阻塞态恢复过来,因此wait方法被唤醒起来的时候会去检测这个状态,当有线程interrupt了它的时候,它就会抛出这个异常从阻塞状态恢复过来。

这里有两点要注意:

如果被interrupt的线程只是创建了,并没有start,那等他start之后进入wait态之后也是不能会恢复的

如果被interrupt的线程已经start了,在进入wait之前,如果有线程调用了其interrupt方法,那这个wait等于什么都没做,会直接跳出来,不会阻塞

被notify(All)的线程有规律吗

这里要分情况:

如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来

如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略

notify执行之后立马唤醒线程吗

其实这个大家可以验证一下,在notify之后写一些逻辑,看这些逻辑是在其他线程被唤起之前还是之后执行,这个是个细节问题,可能大家并没有关注到这个,其实hotspot里真正的实现是退出同步块的时候才会去真正唤醒对应的线程,不过这个也是个默认策略,也可以改的,在notify之后立马唤醒相关线程。

notifyAll是怎么实现全唤起的

或许大家立马想到这个简单,一个for循环就搞定了,不过在jvm里没实现这么简单,而是借助了monitorexit,上面我提到了当某个线程从wait状态恢复出来的时候,要先获取锁,然后再退出同步块,所以notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,同样这这是一个策略的问题,jvm里提供了挨个直接唤醒线程的参数,不过都很罕见就不提了。

wait的线程是否会影响CPU的load负载么?

这个或许是大家比较关心的话题,因为关乎系统性能问题,wait/nofity底层是通过jvm里的park/unpark机制来实现的,在linux下这种机制又是通过pthread_cond_wait/pthread_cond_signal来玩的,因此当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源。

小结

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">小结</span></h3></div>

今天这一节成长记, 你应该掌握如下知识:

1) synchronized锁升级的整个详细的过程

锁的升级流程简单来说是,无锁->偏向锁->自旋锁->重量级锁,除此也有很多其他升级的分支。你一定要记住如下这个图就可以了。

2) synchronized不同锁的核心原理

JVM基于Markword的锁实现机制

偏向锁中的JavaThread指针的作用

轻量级锁(自旋锁)中的LockRecord的作用

重量级锁中的ObjectMonitor的作用

3) wait和notify的实现原理

4) synchronized锁、wait和notify相关细节问题

本文由博客一文多发平台 OpenWrite 发布!

繁茂
19 声望8 粉丝