1. 程序真的是顺序执行的吗?
    1. 优化器优化,会重排序
  2. 线程之间执行的先后顺序及中间过程是不可预知的

乱序程序的分析

  1. package com.mashibing.juc.c_001_03_Ordering;
  2. /**
  3. * 本程序跟可见性无关,曾经有同学用单核也发现了这一点
  4. */
  5. import java.util.concurrent.CountDownLatch;
  6. public class T01_Disorder {
  7. private static int x = 0, y = 0;
  8. private static int a = 0, b = 0;
  9. public static void main(String[] args) throws InterruptedException {
  10. for (long i = 0; i < Long.MAX_VALUE; i++) {
  11. x = 0;
  12. y = 0;
  13. a = 0;
  14. b = 0;
  15. CountDownLatch latch = new CountDownLatch(2);
  16. Thread one = new Thread(new Runnable() {
  17. public void run() {
  18. a = 1;
  19. x = b;
  20. latch.countDown();
  21. }
  22. });
  23. Thread other = new Thread(new Runnable() {
  24. public void run() {
  25. b = 1;
  26. y = a;
  27. latch.countDown();
  28. }
  29. });
  30. one.start();
  31. other.start();
  32. latch.await();
  33. String result = "第" + i + "次 (" + x + "," + y + ")";
  34. if (x == 0 && y == 0) {
  35. System.err.println(result);
  36. break;
  37. }
  38. }
  39. }
  40. }

图解

image.png

  1. 只有再最后一种情况时,即每一个线程中的两句语句都交换了顺序时才会出现都是0的情况
  2. 验证了两个语句之间有一定概率会交换顺序执行(出现可能性不高)
    1. 因为一条java语句可能对应好多条汇编语句,要所有的汇编语句都换过来才可能出现
    2. 两个线程都要换===>概率不高
    3. 能够验证乱序现象的存在
  3. 单线程中写了两句话但未必是先执行第一句再执行第二句

乱序的原因

  1. 简单说,就是为了提高效率
    1. 寄存器的速度比内存快100倍
    2. 下面的例子好比吃饭时烧水后再切菜,而一般在烧水的同时会切菜===>流水线技术!!!并行执行,单线程只要没有数据相关性就可以乱序(在编译阶段可以判断?)
    3. 从微观上讲,第二条指令执行更快,所以可能在第一条指令执行结束之前执行完
    4. cpu为提高效率进行的优化机制===>所以才有乱序

image.png

  1. 不是所有语句都可以乱序,要前后两条语句没有依赖关系,要不能影响单线程的最终一致性
  2. 前后两条语句没有依赖关系时,可能会换

乱序存在的条件

  • as-if-serial好像是序列化执行的
  • 不影响单线程的最终一致性
  • 不存在一致性(谁先执行不影响线程的最终一致性)
  • 看起来序列化,实际上未必序列化
  • 单线程中虽然乱序没有影响,但是多线程中的乱序影响比较严重

存在问题的程序

  1. 可见性问题:
    1. ready设为true后并不会马上停止,有可能也会马上停止(MESI的主动性或者yield同步刷新)
      1. 对ready加volatile修饰
  2. 有序性问题:
    1. number和ready赋值语句可能会换顺序
  1. package com.mashibing.juc.c_001_03_Ordering;
  2. public class T02_NoVisibility {
  3. private static boolean ready = false;
  4. private static int number;
  5. private static class ReaderThread extends Thread {
  6. @Override
  7. public void run() {
  8. while (!ready) {
  9. Thread.yield();
  10. }
  11. System.out.println(number);
  12. }
  13. }
  14. public static void main(String[] args) throws Exception {
  15. Thread t = new ReaderThread();
  16. t.start();
  17. number = 42;
  18. ready = true;
  19. t.join();
  20. }
  21. }

new一个对象所要经过的过程

  1. 由5条java指令构成
    1. 申请内存,赋默认值(半初始化状态
    2. 调初始化方法,赋初始值
    3. 与局部变量的引用创建关联(引用变量指向对象)
    4. 第三步是复制,因为第四步调用的时候需要消耗掉一个指针,先复制一份
    5. 第四步是构造方法

image.png

this对象逸出

  • 以下程序会输出什么? ```java package com.mashibing.juc.c_001_03_Ordering;

public class T03_ThisEscape {

  1. private int num = 8;
  2. public T03_ThisEscape() {
  3. // this存在于局部变量表中(jvm),实际上就是一引用对象
  4. new Thread(() -> System.out.println(this.num)
  5. ).start();
  6. new Thread(() -> System.out.println(num)
  7. ).start();
  8. }
  9. public static void main(String[] args) throws Exception {
  10. new T03_ThisEscape();
  11. // 阻塞,让主线程不结束,确保子线程执行完
  12. System.in.read();
  13. }

} ```

  • 这边有可能会出现问题,虽然做实验可能很难做出来
  • 理论上是有问题的===>可能会输出中间状态0
  • 第三步建立关联是和this建立关联===>而在本程序中调用初始化方法和建立关联可能会互换顺序(有可能换顺序)
  • 先建立与this的关联(此时num=0),再调用初始化方法,而在调用构造方法时new了一个线程(启动时可能还没有赋值为8,所以打印的时候有可能是中间状态0)
  • 这就叫this的中间状态逸出了,逸出到构造方法了(没穿衣服就出来了,穿了一半就出来了,全穿完才出来)
  • ❓要不要加volatile?
  • 因为有this逸出的现象,所有不要在构造方法中new线程然后启动,可以new线程,但是不要启动;可以单独写一个方法,启动线程(不要在构造方法中new线程然后启动)

美团的七连问

  1. 解释对象的创建过程(对象半初始化问题)
    1. 汇编语言就是助记符
  2. 加问DCL与volatile问题(指令重排序)
    1. DCL单例要不要加volatile ===>双重校验锁(Double Check Lock)
    2. 底层禁止排序内存屏障(jvm级别:memoryBarriar、fence)、java禁止重排序volatile
    3. 内存屏障使用汇编里的lock做到的
    4. DCL是什么(开源项目中用的很多)
      1. 双重校验锁
      2. linux中线程切换是CFS调度程序
      3. 判断的性能远远高于锁的性能
      4. 加volatile主要是为了禁止重排序!(保持可见性的同步是次要的)
      5. 为了防止某一线程获取了一个指向半初始化状态的对象(防止出现为0的订单)
      6. 实际中很难出现这个bug,一般只出现在面试中
      7. spring中单例没有这个问题,他会修正这个问题

        java中什么时候可以互换

  • 底层只要指令之间互相不影响,没有依赖关系,并且保证了最终一致性,就可以互换(java中不一样
  • jvm是操作系统上的程序
  • jvm中规定有8种情况不允许重排序(happened-before),除了这8种外其他都可以(new对象没有在这八种中)===>什么时候用力,什么时候放松
  • 内存屏障有很多种===>各种不同的cpu有不同的屏障
  • jvm屏蔽了这些区别
  • JSR内存屏障(jvm要求===>还有相应的底层实现)
    • LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2, 在L oad2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障: 对于这样的语句Store1; StoreStore; Store2, 在Stcre2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • LoadStore屏障: 对于这样的语句oad1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
  • 和垃圾回收器里的读屏障和写屏障没有什么关系(修改内存时需要做一些操作—->类似拦截器?)

image.png

缓存行(缓存一致性)

  • volatile
  • 不做任何措施:不固定
  • 强制刷新操作(比如加锁sout)

JUL工具可以看对象在内存中的存储布局

image.png
类型指针原来是8字节,然后经过压缩变成了4bye(UseCompressedOops)===>Ordinary Object Pointer普通对象指针(32GB内存及以上压缩就不起作用了===>硬件厂商的偷工减料:数据总线的宽度、控制总线、地址总线什么48个???)
image.png

总结

  • 为了提高执行效率,CPU指令可能会乱序执行
  • 乱序执行不得影响单线程的最终一致性

  • as- if -serial:单线程程序看上去象序列化执行

  • 乱序在多线程的情况下可能会产生难于察觉的错误

两个问题

什么时候不能乱序

  • happened-before原则(8条),除这8条之外都有可能换顺序
    • 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循 环结构。
    • 管程锁定规则: -个unlock操作先行发生于后面(时间上)对同-一个锁的lock操作。
    • volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
    • 线程启动规则: Thread的start( )方法先行发生于这个线程的每一个操作。
    • 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线 程的终止。
    • 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Threadinterrupt( )方法检 测线程是否中断
    • 对象终结规则: -个对象的初始化完成先行于发生它的finalize0方法的开始。
    • 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
  • 不要记,底层一条lock语句就全部解决了;这只是jvm层面的规则

    如何解决乱序

    底层内存屏障

    jvm对于底层的规则,可以选也可以不选!
    lock指令必须后面跟一条指令,表明当执行后面这条指令的时候,对总线或者缓存进行锁定;并且这条指令不能是空指令nop,必须有一条;所以就给某个寄存器加了个0(addl),相当于空操作
    image.png

    jvm层级

    jvm是一个规范===>hotspot是一个实现
    不要与底层内存屏障混起来

    使用volatile禁止指令重排序

    volatile修饰的是位置===>是一个内存位置,与顺序无关(而不是内存屏障)
    image.png

  • 不要钻牛角尖===>LoadLoadBarrier放在volatile读之后的原因,感觉没必要?(马老师也不知道)

  • DCL中加volatile之后将引用与对象关联起来就必然在给内部字段赋值(即初始化)之后了!,不会造成虚幻赋值了

🤏随想

  1. 使用jclasslib时要先编译然后把光标定位到main方法里面