关于java:JDK成长记16从0分析你不知道的synchronized底层原理下

4次阅读

共计 17066 个字符,预计需要花费 43 分钟才能阅读完成。

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

如下所示:

这一节咱们认真来剖析下这个过程中,每一步的底层原理。咱们须要用到一个工具包,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 以此类推。大端序反之。

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

能够看到 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 个流程的变动如下图所示:

接着咱们看往后看:

  • 偏差锁已启动:无锁态 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 代码块后,会间接变成偏差锁。如下图所示:

  • 偏差锁未启动:无锁态 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

此流程如下图所示:

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

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

  //-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 这个对象指针)

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

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

很早之前 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 个线程,竞争强烈,当线程获取锁的时候间接就是重量级锁。如下图所示:

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

好了,到这里置信你对 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 的分割。

偏差锁的基本原理

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

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

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

分量锁的 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 中的线程竞争尝试获取锁。

整个过程如下所示:

可能你会问,那尝试加锁这个过程,也就是对_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();}

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

下面过程波及很多细节,须要认真钻研 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 公布!

正文完
 0