并发编程-线程安全性之可见性有序性

线程安全性之可见性有序性

以下是学习中的一些笔记记录,如果有错误希望得到你的指正~ 同时也希望对你有点帮助~

线程可见性问题

public class ThreadVisibilityClass {
    //static boolean flag = false;
    static volatile boolean flag = false;

    public static void main(String[] args) throws Exception{
        new Thread(() -> {
            int i = 0;
            //活性失效
            while (!flag){
                i++;
            }
            System.out.println("i = " +  i);
        }, "Thread-Sub").start();

        TimeUnit.SECONDS.sleep(1L);
        //在main线程中去修改flag的值,没有做其他控制的话,子线程Thread-Sub中无法感知flag被main线程修改
        flag = true;
        System.out.println("main is end...");
        System.out.println(Thread.currentThread().getName());
    }
}

上面就是一个简单的可见性的例子,简单说就是Main线程修改了共享变量的值,Thread-Sub线程却不能及时读取到Main线程修改的最新值,这就是可见性问题

了解可见性本质

导致可见性的根本原因跟下面几方面优化有着密切关系

  • CPU增加高速缓存
  • 操作系统中,增加线程,进程 → 通过CPU时间片切换,提升CPU利用率
  • 编译器(Jvm的深度优化)

CPU高速缓存

  • 缓存行,是CPU和内存交互的最小工作单元,x86系统中,每个缓存行64个字节,CPU在读取内存数据的时候是以块的方式来读取,一次会读取64字节大小的数据,读取64个字节大小的原因其中之一是因为空间局部性原理,CPU会认为这64个字节的数据是接下来会使用到的数据,所以一次性读取,这也是提高CPU效率的一种方式
CPU高速缓存带来的问题
  • 伪共享问题

    • CPU的高速缓存是由多个缓存行来组成的
      伪共享问题图例
      当CPU 一次从内存中读取了 x , y , z 缓存到CPU缓存中,假设 core0 核心只需要修改 x 的值,假设 core1 核心只需要修改 y ,当core0 核心获得执行权的时候,就会使得 core1中缓存的x, y, z失效,当core1 获得执行权的时候也会使得 core0 的执行权失效,来回反复使得效率很低,这就说明在多线程环境下,没有引入对齐填充会使得效率很低。

    引入了对齐填充那么将会读取x 的时候自动填充满 64个字节,读取y 的时候自动填充64个字节,这就不会使得 core0 和 core1 中的缓存来回往复失效,这是一种空间换取时间的思想

    /**
     * CPU高速缓存带来的伪共享问题 模拟多个CPU读取同一个缓存
     *
     * @author zdp
     * @date 2022-05-06 16:35
     */
    public class CacheLineExampleClass implements Runnable {
    
        private int arrayIndex = 0;
    
        public final static long ITERATIONS = 500L * 1000L * 100L;
    
        private static ValueNoPadding[] longs;
    
        public CacheLineExampleClass(final int arrayIndex) {
            this.arrayIndex = arrayIndex;
        }
    
        public static void main(final String[] args) throws Exception {
            //循环创建数组,看消耗时长
            for (int i = 1; i < 10; i++) {
                System.gc();
                final long start = System.currentTimeMillis();
                runTest(i);
                System.out.println(i + " Threads, duration = " + (System.currentTimeMillis() - start));
            }
        }
    
        private static void runTest(int threadNums) throws InterruptedException {
            Thread[] threads = new Thread[threadNums];
            longs = new ValueNoPadding[threadNums];
            for (int i = 0; i < longs.length; i++) {
                longs[i] = new ValueNoPadding();
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(new CacheLineExampleClass(i));
            }
            for (Thread t : threads) {
                t.start();
            }
            for (Thread t : threads) {
                t.join();
            }
        }
    
        @Override
        public void run() {
            long i = ITERATIONS + 1;
            //每个线程去修改 对齐填充类中的 value 变量值
            while (0 != --i) {
                longs[arrayIndex].value = 0L;
            }
        }
    
        /**
         * 对齐填充 一个缓存行 64 字节,long类型8字节,声明8个类型的long变量即可填充满一个缓存行
         */
        public final static class ValuePadding {
            protected long p1, p2, p3, p4, p5, p6, p7;
            protected volatile long value = 0L;
            protected long p9, p10, p11, p12, p13, p14;
            protected long p15;
        }
    
        /**
         * 未对齐填充 Contended 注解配合Jvm参数 -XX:-RestrictContended 使用才会生效
         */
        @Contended
        public final static class ValueNoPadding {
            //protected long p1, p2, p3, p4, p5, p6, p7;
            //8字节
            protected volatile long value = 0L;
        }
    
    }
    
    //对齐填充效果
    1 Threads, duration = 343
    2 Threads, duration = 347
    3 Threads, duration = 318
    4 Threads, duration = 371
    5 Threads, duration = 322
    6 Threads, duration = 488
    7 Threads, duration = 473
    8 Threads, duration = 494
    9 Threads, duration = 507
    //未对齐填充效果
    1 Threads, duration = 320
    2 Threads, duration = 1965
    3 Threads, duration = 1852
    4 Threads, duration = 2390
    5 Threads, duration = 3495
    6 Threads, duration = 3919
    7 Threads, duration = 3473
    8 Threads, duration = 3226
    9 Threads, duration = 2873
    

    造成未对齐填充效率较低的原因就是就是CPU之间不断争抢导致各个CPU之间缓存不断失效,导致每次都从内存重新加载,从而使得效率非常低,对齐填充就解决了这个伪共享问题

  • 缓存一致性问题
    缓存一致性图例
    在Processor 2 中修改了 CPU缓存中的值,而在 Processor 0中却未能及时可见

    • 总线锁思想

      • CPU和外界的交互都需要通过Bus总线,在Bus总线上加上一个互斥锁,可以避免缓存一致性的问题,但是总线锁却违背了多核CPU的概念,因为在总线上加锁,同一时刻只能一个CPU来进行操作,其他CPU资源就浪费了
    • 缓存锁思想 (缓存一致性协议)

      • 在上述图例中,就相当于只针对于 X = 20 这个变量来进行加锁,当其他CPU也需要对该变量进行操作的时候,基于缓存一致性协议来保证数据的一致性,跟总线锁的思想相同,只是锁的粒度不一样,效率也更高了

      • 缓存一致性协议

        MESI, MOSI不同CPU实现不一样,主流的是MESI协议

        • M(Modify): 共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
        • E(Exclusive):缓存的独占状态,数据只缓存在当前CPU缓存中,且没有被修改
        • S(Share):数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
        • I (Invalid): 缓存已经失效

        在CPU的缓存行中,每一个Cache一定会处于以下三种状态之一

        • Shared
        • Exclusive
        • Invalid

        CPU对于读的请求来说,M、E、S状态是可以直接读取的,I 状态下需要从内存中读取

        CPU对于写的请求来说,M、E 状态是可以直接写的,S状态下,需要把其他CPU变成I 失效状态才可以写

        各个CPU间如何实现状态的修改

        • 每个CPU都会基于Snoopy嗅探协议来监听总线上的事件,当CPU接收到消息后,就会做出相应的处理

总结,在各个CPU之间,基于缓存锁,总线锁来保证数据的一致性(解决可见性),但是这个锁怎么加上的呢?在底层其实是会有一个汇编#Lock的指令,它会根据CPU的架构来决定加总线锁还是缓存锁,如果不支持缓存锁就会加总线锁(由CPU决定),那么加上这个Lock指令后就解决了多线程中的可见性问题,也就是说我们在使用Volatile 关键字的时候,底层最终会使用#Lock的汇编指令来解决可见性问题

可以通过汇编指令打印工具,将上述可见性 Demo 打印,可以发现,在加Volatile 关键字后,确实会在底层加上一个Lock的汇编指令

从CPU层面了解有序性问题

/**
 * 线程安全-有序性
 *
 * @author zdp
 * @date 2022-05-05 17:37
 */
public class ThreadOrderClass {

    private static int x = 0,y = 0;
    private static int a = 0,b = 0;

    /**
     * 指令重排序
     *
     * @author zdp
     * @date 2022-05-05 17:55
     */
    public static void main(String[] args) throws Exception{
        int i = 0;
        for (;;){
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            if(x == 0 && y ==0){
                System.out.println("第" + i + "次(" + x + ", " + y + ")");
                break;
            }
        }
    }
}

指令重排序和可见性问题是两种问题,但是Volatile 可以基于#Lock指令来解决这两种问题

CPU层面的指令重排序

CPU层面和Jvm层面,优化执行的执行顺序

  • CPU层面是如何导致指令重排序的

    前言

    ​ 在CPU的优化过程中,CPU基于MESI协议,如果CPU0想去更改某个变量,然后发送失效指令给其他CPU时,等其他的CPU都接受到指令将其置为失效状态,然后CPU0再更改后的值写到主内存中,然后在其他CPU执行失效指令时,CPU其实是处于空闲状态,这就导致了没有很好的利用CPU资源。

    为了尽可能的利用好CPU,引入了第一个优化 Store Buffer(异步的思想)

    • 第一个优化 Store Buffer

      Store Buffer 图例:
      Store Buffer

      在引入了 Store Buffer 之后,CPU0 就先将要更改的变量值写到 Store Buffer 中,然后再发送失效指令让其他CPU失效,接着CPU0 继续做其他的事情,等到收到 其他CPU的反馈说失效指令已经执行完了,然后再将值从 Store Buffer 中 取出,再同步到内存中

    Store Buffer优化带来的问题

    ​ 正是做的这个Store Buffer异步优化,就是导致指令重排序的源头,看下面一个代码

    a = 0;
    
    function(){
        a = 1;
        b = a + 1;
        assert(b == 2);//false
    }
    

    ​ 引入Store Buffer代码执行图例:
    在这里插入图片描述
    ​ 将上面代码套入到 Store Buffer 图例当中来看整体执行为什么 导致 b = 2 为 false

    ​ 在CPU1CPU2 中的缓存中都缓存有 a 变量,此时 a变量为 share 状态

    CPU0开始执行代码 a = 1 时

    CPU0 将 a = 1 写入到 Store Buffer 中,然后发送给 CPU1 一个 a变量的失效指令,CPU0 继续做其他事情,CPU1基于嗅探协议接收到该失效指令返回一个失效答复 invalidate ack (相当于说CPU1通知 CPU0已经接收到该失效指令了)并返回当前CPU1中a的值,此时CPU0将缓存行中的数据更新为,a = 0/E (并处于独占状态,因为CPU1CPU2中的a已经失效了嘛)

    CPU0 开始执行 b = a + 1;

    ​ 因为此时在CPU0中还没有缓存b变量的值,接着就发起一个 read invalidate b 的指令从内存中读取 到 b = 0,然后b保存到CPU0的缓存行中,此时b变量在 CPU0 中处于独占状态,接着直接运算 b = a + 1; a此时在缓存行中的数据为 0 ,所以b = 1,然后CPU0将 b运算后的值写入到缓存行 b = 1/M (b处于修改状态),这时,CPU1 通知CPU0 执行完毕,然后CPU0 从Store Buffer中取出 a = 1 更新到 CPU0 的缓存行,最终 执行结果为 b =1;

    ​ 所以由于CPU的异步优化导致指令重排序的问题。

    • 第二个优化 Store ForWarding

      由于引入Store Buffer优化之后,CPU0已经将 a = 1写入到了 Store Buffer 中,但是发送失效指令之后没有从 Store Buffer 中直接读取,上述代码若是 CPU0直接从 Store Buffer中读取,则没有问题,所以引入 Store Forwarding 之后,会直接发送失效指令之后会直接从 Store Buffer 中读取数据

      Store Forwarding 优化带来的问题,看下面代码:

      int a = 0; b = 0;
      executeToCpu0(){
          a = 1;
          b = 1;
      }
      
      executeToCpu1(){
          while(b == 1){
              assert(a = =1);
          }
      }
      

      引入Store Forwarding 执行上述代码图例:
      在这里插入图片描述
      当两个线程并行执行上述方法 CPU0执行 executeToCpu0 方法 CPU1 执行 executeToCpu1方法,在CPU0中独占 b变量,在CPU1中独占a变量

      1. CPU1执行 while(b == 1){}指令的时候,在CPU1中缓存行中不存在b,所以CPU1发送一个读指令 read b 从其他 CPU 缓存行中读取 b,此时CPU1还没有拿到b的值
      2. CPU0开始执行 a = 1;的指令。此时在CPU0中又不存在a,因为a = 1; 是一个写操作,所以CPU0先将 a = 1 写入到Store Buffer 中,然后发起一个read invalidate a 的失效指令(此时CPU0还未接收到其他CPU执行指令完成的通知)
      3. 紧接着 CPU0 继续执行 b = 1;因为之前b在CPU0中是独占状态,所以此时CPU0执行b = 1会直接修改缓存行中的值 b = 1/M 状态 。
      4. 执行到这里,此时CPU1接收到了 read b 指令的结果,收到了b的值 b = 1;此时CPU0中的b变为共享状态,因为CPU1中也用到了b,此时CPU1执行 b = =1 返回为true;
      5. 接着CPU1执行代码 assert(a==1);
      6. 但是此时 CPU1缓存行中a的值仍然是 0,所以执行 a == 1 返回false;
      7. 此时CPU1接收到2步骤发送的 a 失效指令,CPU1将 a 修改为 I 失效状态
      8. CPU0 收到 CPU1的失效答复,此时 a 只在CPU0缓存行中存在处于独占状态,然后CPU0 直接从 Store Buffer 中读取 a = 1,CPU0 修改缓存行中的 a = 1

      这里导致a = =1为false的原因就是CPU1在接收CPU0发送的a失效指令之前,就读取了CPU1缓存行中a的值,也就是指令重排序导致的问题,到这里CPU的性能已经得到了极大的优化

    • 第三个优化 Invalid Queue 失效队列

      由于Store Buffer 的大小是有限制的,当Store Buffer 满了之后,CPU会处于阻塞状态,所以引入了Invalid Queue,引入Invalid Queue之后,CPU在处理任何MESI缓存一致性协议的状态的时候都必须看Invalid Queue中是否有该缓存行的失效消息没有处理,有就需要去处理,但是Invalid Queue还是存在数据不一致的问题,Invalid Queue的引入使得发送失效指令这个接收结果这个过程变成了异步的方式,也就意味着对Store Buffer中的数据处理的更快了

      Invalid Queue 图例:
      在这里插入图片描述
      还是跟上面一样两个线程并行执行上述方法 CPU0执行 executeToCpu0 方法 CPU1 执行 executeToCpu1方法,在CPU0中独占 b变量,共享a变量,在CPU1中共享a变量

      1. CPU0执行代码 a = 1;此时CPU0先将 a = 1 写入到 Store Buffer 中,然后发送一个invalidate a 的失效指令
      2. CPU1执行代码 while(b == 1){} ,因为CPU1缓存行中不存在b,所以CPU1发送一个read b的指令从CPU0的缓存行中读取
      3. 此时CPU1接收到了CPU0发送过来的a失效指令,CPU1直接将该指令消息丢到 Invalid Queue 失效队列中
      4. CPU0 收到 CPU1返回的Invalidate ack,立即读取Store Buffer中a的值,同步到CPU0的缓存行中,至此CPU0中 a = 1/M ,并且处于修改状态
      5. 紧接着CPU0执行代码 b = 1;因为b在 CPU0中是独占状态,CPU0可以直接修改b的值 b = 1/M,并且处于修改状态
      6. 到这,CPU1 发送的read b 指令,接收到CPU0 的返回,b =1,此时CPU0中b变更为共享状态
      7. CPU1执行 b = =1 返回 true ,CPU1 接着执行代码 assert(a == 1)
      8. CPU1在执行 a = =1的时候,由于缓存行中的 a = 0(CPU1丢入到Invalid Queue失效队列中的 a失效指令还未来得及处理),所以CPU1 执行代码 a = = 1;返回false
      9. 最后CPU1处理Invalid Queue中的消息,将a置为失效状态

      上述导致代码 assert(a == 1)为false是因为,在处理丢入到Invalid Queue中的消息之前,CPU1读取了缓存行中的 a

    • 第四个优化 内存屏障指令

      在引入Store Buffer/Store Forwarding 之后,解决了多CPU缓存同步带来的CPU阻塞问题(就是基于MESI协议,一个变量为共享Share状态,当CPU接收到一个写操作的时候,需要将其他CPU中该变量置为I 失效状态,这个操作在没有引入Store Buffer 之前是同步的操作),但是又带来了指令执行顺序带来的可见性问题(各个CPU之间由于指令重排序导致的执行先后顺序导致 共享变量的 可见性问题(就比如失效队列中消息还未处理,就读取了缓存行中的数据)),为了解决Store Buffer 带来的指令执行顺序带来的可见性问题,因为Store Buffer/ Store Forwarding带来的问题在CPU硬件层面已经无法优化,因此引入了内存屏障指令,不同操作系统 内存屏障指令也不相同

      • 内存屏障

        X86/64 系统架构中

        • 写屏障:SFence (Save Fence),写屏障指令。在SFence指令前的写操作必须在SFence指令后的写操作前完成。
        • 读屏障:IFence (Load Fence),读屏障指令。在IFence指令前的读操作必须在IFence指令后的读操作前完成。
        • 全屏障:MFence (Modify/Mix),全屏障指令,在MFence前的读写操作必须在MFence指令后的读写操作前完成。

        在Linux系统中,将这三种指令分别封装成了,smp_wmb-写屏障,smp_rmb-读屏障,smp_mb-读写屏障三个方法,调用这些方法就可以出发相应的屏障指令执行

      前面说到为了解决多线程带来的可见性问题,使用了#Lock指令,而加上Volatile会在底层中加上一个汇编指令#Lock,这里#Lock指令不但能解决多线程带来的可见性问题,它还相当于一个全屏障指令 Full Barrier ,执行时会所主内存子系统来确保执行顺序,所以Volatile 关键字能够解决可见性和有序性问题

      那到底这个屏障指令是如何工作的呢?回到上面代码:

      int a = 0; b = 0;
      executeToCpu0(){
          a = 1;
          b = 1;
      }
      
      executeToCpu1(){
          while(b == 1){
              assert(a = =1);
          }
      }
      
      //加入屏障指令,也就是在执行a == 1代码之前,必须先完成 a 的读取操作
      int a = 0; b = 0;
      executeToCpu0(){
          a = 1;
          smp_wmb();
          b = 1;
      }
      
      executeToCpu1(){
          while(b == 1){
              smp_rmb();
              assert(a = =1);
          }
      }
      

JMM (Java Memory Model) 模型

由于在不同的CPU架构中,实现内存屏障的指令不同

  • #Lock
  • #StoreBuffer
  • #LoadBuffer

在不同操作系统上都需要保证可见性,所以引入了JMM模型,解决不同操作系统之间的差异化

JVM由于是跨平台的实现,为了解决不同平台上都能达到线程安全的目的,引出了JMM(Java 内存模型),JMM就是一种符合内存模型规范,屏蔽了各种硬件和操作系统访问差异,使得Java程序在各种平台下对内存的访问都能保证效果一致的规范

JMM 也是为了解决有序性、可见性问题,JMM本身并没有解决CPU层面优化带来的可见性、有序性问题。我们平时开发在解决这些问题的时候,是用了volatile、final、synchronized这些关键字,那么这些关键字底层指令是怎么封装的,如何去保证可见性的呢?

  • 在CPU层面提供了内存屏障指令,这些内存屏障指令是需要被调用的,JMM里面对CPU层面内存屏障指令做了封装,提供了下面这些统一的内存屏障指令,相当于说在任何操作系统下都使用下面这些统一的内存屏障指令,JVM相当于用了一个策略模式,针对不同的系统,对于底层的屏障指令进行不同的实现

    屏障类型指令示例说明
    LoadLoad BarriersLoad1;LoadLoad;Load2确保Load1数据的装载先于Load2及所有后续装载指令的装载(这两个读操作不允许重排序)
    StoreStore BarriersStore1;StoreStore;Store2确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 (这两个写操作不能重排序)
    LoadStore BarriersLoad1;LoadStore;Store2 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存(Load1的读操作优先于Store2的写操作)
    StoreLoad BarriersStore1;StoreLoad;Load2确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令
  • x86结构中CPU层面提供#Lock指令

  • JVM层面对上述4中屏障指令的实现

inline void OrderAccess::loadload() { 
    acquire(); 
} 
inline void OrderAccess::storestore() { 
    elease(); 
} 
inline void OrderAccess::loadstore() { 
    acquire(); 
} 
inline void OrderAccess::storeload() { 
    fence(); 
}
  • orderAccess_linux_x86.inline Linux系统x86 CPU架构

    Lock 也是一个内存屏障

    inline void OrderAccess::fence() { 
        //如果是多核CPU,会加一个lock指令,锁住缓存行,基于缓存一致性协议来保证可见性
        if (os::is_MP()) { 
        // always use locked addl since mfence is sometimes expensive 
    #ifdef AMD64 
    	__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); 
    #else
    	__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); 
    #endif 
    	} 
    }
    
  • orderAccess_linux_sparc.inline Linux系统sparc CPU架构

    #StoreLoad也是一个内存屏障

    inline void OrderAccess::fence() { 
        __asm__ volatile ("membar #StoreLoad" : : :); 
    }
    

不同CPU结构内存指令屏障不同

  • JVM字节指令中可以看出加了volatile关键字之后,flag中会增加一个 ACC_VOLATILE 的标识

    //javap -v VolatileSeeLock.class
        
    public static volatile boolean flag;
    	descriptor: Z
    	flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
    
    //在JVM中使用putstatic 修改变量值的时候会去判断变量是否被volatile修饰
           
    if(cache -> is_volatile){
        //....
        //如果是volatile修饰的变量将会加一个storeload屏障,storeload上面会调用fence()全屏障指令,所以在底层指令中会加入一个#Lock,所以汇编指令也会输出一个lock,storeload指令就保证了变量的写操作一定会对后续操作可见
        OrderAccess::storeload();
    }
    
    //而这个is_volatile的判断是由 flag -> ACC_VALATILE来决定
    bool is_volatile () const { 
        return (_flags & JVM_ACC_VOLATILE ) != 0; }
            
    

JVM是一种跨平台实现,为了保证多线程情况下能够在不同平台都保证线程安全性,引出了JMMJMM是一种内存模型规范,屏蔽了不同操作系统之间的差异,保证Java程序在不同平台下的内存访问都能保证一致性。JMM对不同CPU架构的内存屏障指令做了统一的定义,在JVM中是怎么实现的呢?在JVM中也有这四种定义,JVM在实现这四种规范的时候,针对不同的CPU架构做了不同的实现,也就是针对不同的CPU架构,调用了其相对应的内存屏障指令,达到了操作系统CPU结构下访问内存都能达到一致性

  • 加了volatile关键字后,什么情况下会导致重排序

    volatile重排序规则表

    是否能重排序第二个操作第二个操作第二个操作
    第一个操作普通读/写volatile读volatile写
    普通读/写YESYESNO

| volatile读 | NO | NO | NO |
| volatile写 | YES | NO | NO |

举例:

//第一个volatile读操作
volatile int a = 0//第二个普通读
int b = 0

第一个操作是读volatile修饰的变量

第二个操作是读普通变量

那么这a,b变量是不允许重排序的

Happens-Before 模型

在某些情况下,不需要通过volatile关键字,也能保证多线程环境下的可见性和有序性。在Jdk1.5开始,引入了一个 Happens-Before 的概念来阐述多个线程操作共享变量的可见性问题。所以我们可以认为在Jvm中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在Happens-Before关系,两个操作可以是一个线程,也可以是不同的线程

程序顺序规则

可以简单认为是as-if-serial,不管怎么重排序,单线程的程序的执行结果不能改变

  • 处理器不能对存在依赖关系的操作进行重排序,因为重排序会改变程序的执行结果
  • 对于没有依赖关系的指令,即便是重排序,也不会改变单线程环境下的执行结果

举例:

int a = 1;//操作A
int b = 2;//操作B
int c = a * b; //操作C

A和B之间允许重排序,但是C不允许重排序,因为存在依赖关系。根据as-if-serial,在单线程环境下,不管怎么重排序,最终执行的结果都不会发生变化

传递性规则

用上述程序顺序规则代码说明:

  • A happens-before B
  • B happens-before C
  • 那么A happens-before C

这里A happens-before B 不一定存在,也可能是 B happens-before A ,JMM不要求A一定要在B之前执行,但是要求前一个操作的执行结果对后一个操作可见。这里操作A的执行结果不需要对操作B可见,并且重排序操作A和操作B的执行结果与 A happens-before B顺序执行的结果一致,这种情况下,是允许重排序的

Volatile变量规则

对于volatile修改的变量的写操作,一定happens- before后续对volatile变量的读操作,这个是因为volatile底层通过内存屏障机制防止了指令重排,基于前面两种规则再结合volatile规则来分析下面代码的执行顺序

public class VolatileExample { 
    int a=0; 
    volatile boolean flag=false; 
    public void writer(){ 
        a=1; 			//1 
        flag=true; 		//2 
    }
    public void reader(){ 
        if(flag) { 		//3 
            int i=a; 	//4 
        } 
    }
}
  • 根据程序规则 1 happens-before 2 、 3 happens-before 4
  • 2 happens-before 3 这是由volatile 规则产生的,对一个volatile 变量的读,总能看到任意线程对这个volatile 变量的写入
  • 1 happens-before 4 根据传递性规则以及volatile的内存屏障策略共同保证

那么最后执行结果时,如果线程B执行reader() 方法时,如果flag 为true,那么意味着 i = 1成立

上述代码中1和2操作不存在重排序问题,因为volatile修改的重排序规则存在,如果第一个操作是普通变量的读、写,第二个操作是volatile的写操作,那么这两个操作之间是不允许重排序的,见上述volatile 重排序规则表

监视器锁规则

一个线程对于一个锁的释放锁操作,一定happens-before与后续线程对这个锁的加锁操作

int x=10; 
synchronized (this) { // 此处自动加锁 
    // x 是共享变量, 初始值 =10 
    if (this.x < 12) { 
        this.x = 12; 
    } 
} // 此处自动解锁

假设x的初始值是10,线程A执行完代码块后,x的值变成12,执行完成后释放锁。线程B进入代码块,能够看到线程A对x的写操作,也就是线程B看到x的值为12

Start规则

如果线程A执行操作 ThreadB.start(),那么线程A的ThreadB.start()之前的操作happens-before线程B中的任意操作

public StartDemo{ 
    int x=0; 
    Thread t1 = new Thread(()->{ 
        // 主线程调用 t1.start() 之前 
        // 所有对共享变量的修改,此处皆可见 
        // 此例中,x==10 
    }); 
    // 此处对共享变量 x修改 
    x = 10; 
    // 主线程启动子线程 
    t1.start(); 
}

在上面代码中,在main线程中修改了x的值为10,那么在main线程调用start() 方法执行子线程的时候,x = 10的这个操作一定先于子线程中的任意操作,所以子线程中x的值一定为10

Join规则

如果线程A执行操作ThreadB.join(),并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()返回之后的操作

int i = 0;
Thread t1 = new Thread(()->{ 
    // 此处对共享变量 x 修改 
    x= 100; 
}); 
// 例如此处对共享变量修改, 
// 则这个修改结果对线程 t1 可见 
// 主线程启动子线程 
t1.start(); 
t1.join() 
// 子线程所有对共享变量的修改 
// 在主线程调用 t1.join() 之后皆可见 
// 此例中,x==100
if(i == 100){
    System.out.println("x=100");
}

在上面代码中,main线程调用子线程t1的join() 方法,那么子线程中的任意操作一定会先于main线程中join()方法之后的操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值