Monitor 原理

Monitor 被翻译为监视器管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下
image.png

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一
    个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
    EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
    wait-notify 时会分析

    注意

    • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
    • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理

  1. static final Object lock = new Object();
  2. static int counter = 0;
  3. public static void main(String[] args) {
  4. synchronized (lock) {
  5. counter++;
  6. }
  7. }

对应的字节码为

  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=2, locals=3, args_size=1
  6. 0: getstatic #2 // <- lock引用 (synchronized开始)
  7. 3: dup // 为什么要复制并保存一份,是为了后面解锁,你不能只加锁,不解锁
  8. 4: astore_1 // lock引用 -> slot 1
  9. 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
  10. 6: getstatic #3 // <- i
  11. 9: iconst_1 // 准备常数 1
  12. 10: iadd // +1
  13. 11: putstatic #3 // -> i
  14. 14: aload_1 // <- lock引用 // 拿到刚刚存储的临时变量 //根据MarkWord对象头找到Monitor,然后调用monitorexit指令
  15. 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList // 这里重置MarkWord,对于分代年龄什么的,都在Monitor中了,可以取出来
  16. 16: goto 24 // 明明已经结束了 为什么还有下面这些?? 因为上面释放锁是正常情况 这里考虑了异常
  17. 19: astore_2 // e -> slot 2 // 基本就是保存异常对象;取出lock引用,重置和唤醒;加载异常对象;抛出异常;return
  18. 20: aload_1 // <- lock引用
  19. 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
  20. 22: aload_2 // <- slot 2 (e)
  21. 23: athrow // throw e
  22. 24: return // 这样就保证了加了锁,一定可以解开。
  23. Exception table:
  24. from to target type
  25. 6 16 19 any
  26. 19 22 19 any
  27. LineNumberTable:
  28. line 8: 0
  29. line 9: 6
  30. line 10: 14
  31. line 11: 24
  32. LocalVariableTable:
  33. Start Length Slot Name Signature
  34. 0 25 0 args [Ljava/lang/String;
  35. StackMapTable: number_of_entries = 2
  36. frame_type = 255 /* full_frame */
  37. offset_delta = 19
  38. locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
  39. stack = [ class java/lang/Throwable ]
  40. frame_type = 250 /* chop */
  41. offset_delta = 4

根据上面我们知道,对于synchronized加锁,其实是依赖Monitor对象,这个是由操作系统提供的;其实是非常耗费性能的。因此从jdk1.6就改进了这个synchronized获取锁的方式,优化。

注意 方法级别的 synchronized 不会在字节码指令中有所体现

synchronized 原理

1. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁

  1. static final Object obj = new Object();
  2. public static void method1() {
  3. synchronized( obj ) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public static void method2() {
  9. synchronized( obj ) {
  10. // 同步块 B
  11. }
  12. }
  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

image.png

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存
    入锁记录

image.png

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

image.png

  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程(本来就是00)
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数(这种失败没有关系,是锁重入)

image.png

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
    入计数减一(这个直接清除锁记录即可。)

image.png

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

      2. 锁膨胀

      如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
      1. static Object obj = new Object();
      2. public static void method1() {
      3. synchronized( obj ) {
      4. // 同步块
      5. }
      6. }
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image.png

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

image.png

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁
    流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

    3. 自旋优化(有一点疑问)

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
    自旋重试成功的情况
    image.png
    自旋重试失败的情况
    image.png

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

    4. 偏向锁

    轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
    Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
    例如: ```java static final Object obj = new Object(); public static void m1() { synchronized( obj ) {
    1. // 同步块 A
    2. m2();
    } }

public static void m2() { synchronized( obj ) { // 同步块 B m3(); } }

public static void m3() { synchronized( obj ) { // 同步块 C } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/12786164/1636968659466-6501a048-25db-4fee-b092-98b2e5cd1bd1.png#clientId=u01b67410-16e5-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u10b9bfd3&name=image.png&originHeight=675&originWidth=871&originalType=binary&ratio=1&rotation=0&showTitle=false&size=136769&status=done&style=none&taskId=uff6ca626-f3b3-4662-9da6-e8a107f71d2&title=)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12786164/1636968682588-8799957f-f73c-4775-a7e6-93f69cdd9d93.png#clientId=u01b67410-16e5-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u7810387e&name=image.png&originHeight=476&originWidth=864&originalType=binary&ratio=1&rotation=0&showTitle=false&size=75939&status=done&style=none&taskId=u07ab947b-36d5-4ea3-98f6-ad020326b2b&title=)
  2. <a name="Kgo1P"></a>
  3. ### 偏向状态
  4. 回忆一下对象头格式<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12786164/1636968715287-5cfe187c-89df-4a36-aec5-c70fdc1009ba.png#clientId=u01b67410-16e5-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u07d94307&name=image.png&originHeight=408&originWidth=1025&originalType=binary&ratio=1&rotation=0&showTitle=false&size=85530&status=done&style=none&taskId=uff21ca39-33d8-4445-854f-9f539410911&title=)<br />一个对象创建时:
  5. - 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的threadepochage 都为 0
  6. - 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -<br />XX:BiasedLockingStartupDelay=0 来禁用延迟
  7. - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、<br />age 都为 0,第一次用到 hashcode 时才会赋值
  8. 1 测试延迟特性<br />2 测试偏向锁
  9. ```java
  10. class Dog {}

利用 jol 第三方工具来查看对象头信息(注意这里我扩展了 jol 让它输出更为简洁)

  1. // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
  2. public static void main(String[] args) throws IOException {
  3. Dog d = new Dog();
  4. ClassLayout classLayout = ClassLayout.parseInstance(d);
  5. new Thread(() -> {
  6. log.debug("synchronized 前");
  7. System.out.println(classLayout.toPrintableSimple(true));
  8. synchronized (d) {
  9. log.debug("synchronized 中");
  10. System.out.println(classLayout.toPrintableSimple(true));
  11. }
  12. log.debug("synchronized 后");
  13. System.out.println(classLayout.toPrintableSimple(true));
  14. }, "t1").start();
  15. }

输出

  1. 11:08:58.117 c.TestBiased [t1] - synchronized
  2. 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  3. 11:08:58.121 c.TestBiased [t1] - synchronized
  4. 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
  5. 11:08:58.121 c.TestBiased [t1] - synchronized
  6. 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

注意 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

3)测试禁用
在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
输出

  1. 11:13:10.018 c.TestBiased [t1] - synchronized
  2. 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  3. 11:13:10.021 c.TestBiased [t1] - synchronized
  4. 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
  5. 11:13:10.021 c.TestBiased [t1] - synchronized
  6. 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

4) 测试 hashCode

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成

    撤销 - 调用对象 hashCode

    调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被
    撤销

  • 轻量级锁会在锁记录中记录 hashCode

  • 重量级锁会在 Monitor 中记录 hashCode

在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
输出

  1. 11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
  2. 11:22:10.391 c.TestBiased [t1] - synchronized
  3. 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
  4. 11:22:10.393 c.TestBiased [t1] - synchronized
  5. 00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
  6. 11:22:10.393 c.TestBiased [t1] - synchronized
  7. 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001

撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

  1. private static void test2() throws InterruptedException {
  2. Dog d = new Dog();
  3. Thread t1 = new Thread(() -> {
  4. synchronized (d) {
  5. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  6. }
  7. synchronized (TestBiased.class) {
  8. TestBiased.class.notify();
  9. }
  10. // 如果不用 wait/notify 使用 join 必须打开下面的注释
  11. // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
  12. /*try {
  13. System.in.read();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }*/
  17. }, "t1");
  18. t1.start();
  19. Thread t2 = new Thread(() -> {
  20. synchronized (TestBiased.class) {
  21. try {
  22. TestBiased.class.wait();
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  28. synchronized (d) {
  29. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  30. }
  31. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  32. }, "t2");
  33. t2.start();
  34. }

输出

  1. [t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
  2. [t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
  3. [t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
  4. [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

撤销 - 调用 wait/notify

  1. public static void main(String[] args) throws InterruptedException {
  2. Dog d = new Dog();
  3. Thread t1 = new Thread(() -> {
  4. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  5. synchronized (d) {
  6. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  7. try {
  8. d.wait();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
  13. }
  14. }, "t1");
  15. t1.start();
  16. new Thread(() -> {
  17. try {
  18. Thread.sleep(6000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. synchronized (d) {
  23. log.debug("notify");
  24. d.notify();
  25. }
  26. }, "t2").start();
  27. }

输出

  1. [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
  2. [t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
  3. [t2] - notify
  4. [t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010

批量重定向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

  1. private static void test3() throws InterruptedException {
  2. Vector<Dog> list = new Vector<>();
  3. Thread t1 = new Thread(() -> {
  4. for (int i = 0; i < 30; i++) {
  5. Dog d = new Dog();
  6. list.add(d);
  7. synchronized (d) {
  8. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  9. }
  10. }
  11. synchronized (list) {
  12. list.notify();
  13. }
  14. }, "t1");
  15. t1.start();
  16. Thread t2 = new Thread(() -> {
  17. synchronized (list) {
  18. try {
  19. list.wait();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. log.debug("===============> ");
  25. for (int i = 0; i < 30; i++) {
  26. Dog d = list.get(i);
  27. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  28. synchronized (d) {
  29. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  30. }
  31. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  32. }
  33. }, "t2");
  34. t2.start();
  35. }

输出

  1. [t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  2. [t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  3. [t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  4. [t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  5. [t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  6. [t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  7. [t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  8. [t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  9. [t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  10. [t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  11. [t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  12. [t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  13. [t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  14. [t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  15. [t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  16. [t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  17. [t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  18. [t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  19. [t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  20. [t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  21. [t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  22. [t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  23. [t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  24. [t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  25. [t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  26. [t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  27. [t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  28. [t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  29. [t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  30. [t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  31. [t2] - ===============>
  32. [t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  33. [t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  34. [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  35. [t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  36. [t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  37. [t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  38. [t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  39. [t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  40. [t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  41. [t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  42. [t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  43. [t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  44. [t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  45. [t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  46. [t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  47. [t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  48. [t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  49. [t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  50. [t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  51. [t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  52. [t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  53. [t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  54. [t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  55. [t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  56. [t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  57. [t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  58. [t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  59. [t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  60. [t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  61. [t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  62. [t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  63. [t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  64. [t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  65. [t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  66. [t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  67. [t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  68. [t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  69. [t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  70. [t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  71. [t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  72. [t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  73. [t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  74. [t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  75. [t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  76. [t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  77. [t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  78. [t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  79. [t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  80. [t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  81. [t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  82. [t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  83. [t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  84. [t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  85. [t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  86. [t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  87. [t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
  88. [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  89. [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  90. [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  91. [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  92. [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  93. [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  94. [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  95. [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  96. [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  97. [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  98. [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  99. [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  100. [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  101. [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  102. [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  103. [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  104. [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  105. [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  106. [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  107. [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  108. [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  109. [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  110. [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  111. [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  112. [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  113. [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  114. [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  115. [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  116. [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  117. [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  118. [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  119. [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
  120. [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  121. [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向

  1. static Thread t1,t2,t3;
  2. private static void test4() throws InterruptedException {
  3. Vector<Dog> list = new Vector<>();
  4. int loopNumber = 39;
  5. t1 = new Thread(() -> {
  6. for (int i = 0; i < loopNumber; i++) {
  7. Dog d = new Dog();
  8. list.add(d);
  9. synchronized (d) {
  10. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  11. }
  12. }
  13. LockSupport.unpark(t2);
  14. }, "t1");
  15. t1.start();
  16. t2 = new Thread(() -> {
  17. LockSupport.park();
  18. log.debug("===============> ");
  19. for (int i = 0; i < loopNumber; i++) {
  20. Dog d = list.get(i);
  21. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  22. synchronized (d) {
  23. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  24. }
  25. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  26. }
  27. LockSupport.unpark(t3);
  28. }, "t2");
  29. t2.start();
  30. t3 = new Thread(() -> {
  31. LockSupport.park();
  32. log.debug("===============> ");
  33. for (int i = 0; i < loopNumber; i++) {
  34. Dog d = list.get(i);
  35. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  36. synchronized (d) {
  37. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  38. }
  39. log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
  40. }
  41. }, "t3");
  42. t3.start();
  43. t3.join();
  44. log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
  45. }

参考资料
https://github.com/farmerjohngit/myblog/issues/12
https://www.cnblogs.com/LemonFive/p/11246086.html
https://www.cnblogs.com/LemonFive/p/11248248.html
偏向锁论文

5. 锁消除

  1. @Fork(1)
  2. @BenchmarkMode(Mode.AverageTime)
  3. @Warmup(iterations=3)
  4. @Measurement(iterations=5)
  5. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  6. public class MyBenchmark {
  7. static int x = 0;
  8. @Benchmark
  9. public void a() throws Exception {
  10. x++;
  11. }
  12. @Benchmark
  13. public void b() throws Exception {
  14. Object o = new Object();
  15. synchronized (o) {
  16. x++;
  17. }
  18. }
  19. }

java -jar benchmarks.jar

  1. Benchmark Mode Samples Score Score error Units
  2. c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
  3. c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op

java -XX:-EliminateLocks -jar benchmarks.jar

  1. Benchmark Mode Samples Score Score error Units
  2. c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
  3. c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op

锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

wait notify 原理

image.png

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

    join 原理

    这个讲完,直接讲join 因为其用的就是刚刚的保护性暂停模式 区别是保护性暂停模式:一个线程等待另一个线程的结果 join是一个线程等待另一个线程的结束

是调用者轮询检查线程 alive 状态

  1. t1.join();

等价于下面的代码

  1. synchronized (t1) {
  2. // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
  3. while (t1.isAlive()) {
  4. t1.wait(0);
  5. }
  6. }

注意 join 体现的是【保护性暂停】模式,请参考之 看下源码,和前面的保护性暂停模式增强版一模一样。

park unpark 原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中
    的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

image.png
1. 当前线程调用 Unsafe.park() 方法
2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(没有干粮:thread0就进入mutex,mutex也相当于是一个对象,其里面有一个等待队列就是cond)
3. 线程进入 _cond 条件变量阻塞
4. 设置 _counter = 0
image.png
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0
image.png
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _counter 为 0

CPU缓存结构原理

1. CPU缓存结构

image.png
查看 cpu 缓存

  1. root@yihang01 ~ lscpu
  2. Architecture: x86_64
  3. CPU op-mode(s): 32-bit, 64-bit
  4. Byte Order: Little Endian
  5. CPU(s): 1
  6. On-line CPU(s) list: 0
  7. Thread(s) per core: 1
  8. Core(s) per socket: 1
  9. Socket(s): 1
  10. NUMA node(s): 1
  11. Vendor ID: GenuineIntel
  12. CPU family: 6
  13. Model: 142
  14. Model name: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
  15. Stepping: 11
  16. CPU MHz: 1992.002
  17. BogoMIPS: 3984.00
  18. Hypervisor vendor: VMware
  19. Virtualization type: full
  20. L1d cache: 32K
  21. L1i cache: 32K
  22. L2 cache: 256K
  23. L3 cache: 8192K
  24. NUMA node0 CPU(s): 0

速度比较
image.png
查看cpu缓存行

  1. root@yihang01 ~ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
  2. 64

cpu 拿到的内存地址格式是这样的

  1. [高位组标记][低位索引][偏移量]

image.png

2. CPU缓存读

读取数据流程如下

  • 根据低位,计算在缓存中的索引
  • 判断是否有效

    • 0 去内存读取新数据更新缓存行
    • 1 再对比高位组标记是否一致
      • 一致,根据偏移量返回缓存数据
      • 不一致,去内存读取新数据更新缓存行

        3. CPU缓存一致性

        MESI 协议
        1. E、S、M 状态的缓存行都可以满足 CPU 的读请求
        2. E 状态的缓存行,有写请求,会将状态改为 M,这时并不触发向主存的写
        3. E 状态的缓存行,必须监听该缓存行的读操作,如果有,要变为 S 状态
        image.png
        4. M 状态的缓存行,必须监听该缓存行的读操作,如果有,先将其它缓存(S 状态)中该缓存行变成 I 状态(即
        6. 的流程),写入主存,自己变为 S 状态
        5. S 状态的缓存行,有写请求,走 4. 的流程
        6. S 状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为 I 状态
        7. I 状态的缓存行,有读请求,必须从主存读取
        image.png

        4. 内存屏障

        Memory Barrier(Memory Fence)
  • 可见性

    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

image.png

指令级并行原理

1. 名词

Clock Cycle Time

主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的
Cycle Time 是 1s
例如,运行一条加法指令一般需要一个时钟周期时间

CPI

有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数

IPC

IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU执行时间

程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

2. 鱼罐头的故事

加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…
image.png
可以将每个鱼罐头的加工流程细分为 5 个步骤:

  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟

image.png

3. 指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段 (其实就像是一条指令拆分成了五条,当然不同CPU拆分条数不同,但现在应该大部分是五条
image.png
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位。

提示: 分阶段,分工是提升效率的关键!

指令重排的前提是,重排指令不能影响结果,例如

  1. // 可以重排的例子
  2. int a = 10; // 指令1
  3. int b = 20; // 指令2
  4. System.out.println( a + b );
  5. // 不能重排的例子
  6. int a = 10; // 指令1
  7. int b = a - 5; // 指令2

参考: Scoreboarding and the Tomasulo algorithm (which is similar to scoreboarding but makes use of
register renaming) are two of the most common techniques for implementing out-of-order execution
and instruction-level parallelism.

4. 支持流水线的优化器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。
提示: 奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃

image.png

5. SuperScalar 处理器

大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC> 1
image.png
image.png

volatile原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

    1. 如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    1. public void actor2(I_Result r) {
    2. num = 2;
    3. ready = true; // ready 是 volatile 赋值带写屏障
    4. // 写屏障
    5. }
  • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

    1. public void actor1(I_Result r) {
    2. // 读屏障
    3. // ready 是 volatile 读取值带读屏障
    4. if(ready) {
    5. r.r1 = num + num;
    6. } else {
    7. r.r1 = 1;
    8. }
    9. }

    image.png

    2. 如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    1. public void actor2(I_Result r) {
    2. num = 2;
    3. ready = true; // ready 是 volatile 赋值带写屏障
    4. // 写屏障
    5. }
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    1. public void actor1(I_Result r) {
    2. // 读屏障
    3. // ready 是 volatile 读取值带读屏障
    4. if(ready) {
    5. r.r1 = num + num;
    6. } else {
    7. r.r1 = 1;
    8. }
    9. }

    image.png
    还是那句话,不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去

  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

image.png

3. double-checked locking 问题

以著名的 double-checked locking 单例模式为例

  1. public final class Singleton {
  2. private Singleton() { }
  3. private static Singleton INSTANCE = null;
  4. public static Singleton getInstance() {
  5. if(INSTANCE == null) { // t2
  6. // 首次访问会同步,而之后的使用没有 synchronized
  7. synchronized(Singleton.class) {
  8. if (INSTANCE == null) { // t1
  9. INSTANCE = new Singleton();
  10. }
  11. }
  12. }
  13. return INSTANCE;
  14. }
  15. }

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

  1. 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  2. 3: ifnonnull 37
  3. 6: ldc #3 // class cn/itcast/n5/Singleton
  4. 8: dup
  5. 9: astore_0
  6. 10: monitorenter
  7. 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  8. 14: ifnonnull 27
  9. 17: new #3 // class cn/itcast/n5/Singleton
  10. 20: dup
  11. 21: invokespecial #4 // Method "<init>":()V
  12. 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  13. 27: aload_0
  14. 28: monitorexit
  15. 29: goto 37
  16. 32: astore_1
  17. 33: aload_0
  18. 34: monitorexit
  19. 35: aload_1
  20. 36: athrow
  21. 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  22. 40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
image.png
关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

4. double-checked locking 解决

  1. public final class Singleton {
  2. private Singleton() { }
  3. private static volatile Singleton INSTANCE = null;
  4. public static Singleton getInstance() {
  5. // 实例没创建,才会进入内部的 synchronized代码块
  6. if (INSTANCE == null) {
  7. synchronized (Singleton.class) { // t2
  8. // 也许有其它线程已经创建实例,所以再判断一次
  9. if (INSTANCE == null) { // t1
  10. INSTANCE = new Singleton();
  11. }
  12. }
  13. }
  14. return INSTANCE;
  15. }
  16. }

字节码上看不出来 volatile 指令的效果

  1. // -------------------------------------> 加入对 INSTANCE 变量的读屏障
  2. 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  3. 3: ifnonnull 37
  4. 6: ldc #3 // class cn/itcast/n5/Singleton
  5. 8: dup
  6. 9: astore_0
  7. 10: monitorenter -----------------------> 保证原子性、可见性
  8. 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  9. 14: ifnonnull 27
  10. 17: new #3 // class cn/itcast/n5/Singleton
  11. 20: dup
  12. 21: invokespecial #4 // Method "<init>":()V
  13. 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  14. // -------------------------------------> 加入对 INSTANCE 变量的写屏障
  15. 27: aload_0
  16. 28: monitorexit ------------------------> 保证原子性、可见性
  17. 29: goto 37
  18. 32: astore_1
  19. 33: aload_0
  20. 34: monitorexit
  21. 35: aload_1
  22. 36: athrow
  23. 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  24. 40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面
两点:

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

image.png