美文网首页
12.3 内存模型

12.3 内存模型

作者: 奋斗_2339 | 来源:发表于2020-05-13 14:23 被阅读0次

    1. 主内存与工作内存

    线程、主内存、工作内存之间的关系.png

    Java线程之间通信的话,必须经过2个步骤

    • 1)线程A把本地内存中更新过的共享变量刷新到主内存中去
    • 2)线程B到主内存中去读取线程A之前更新过的共享变量

    2.从源码到指令序列的重排序

    • 2.1:编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
    • 2.2:指令级并行重排序:现在处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其指令的执行顺序。
    • 2.3:内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。 重排序.png
      这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器(2.1),JMM的编译器规则会禁止特定类型的编译器重排序。对于处理器的重排序(2.2和2.3),JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Fence)指令来禁止特定类型的处理器重排序。

    3.as-if-serial 和 happens-before

    \color{red}{3.1.单线程程序(as-if-serial)}语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变

    PS:为了遵循该语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果

    \color{red}{3.2.happens-before}语义规则:
    • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
    • 监视器锁规则:对一个锁的解锁,happens-before于随后对这种锁的枷锁。
    • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 传递性:如果A happens-before B,且B happens-before C, 那么A happens-before C。
    • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
    • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
    \color{red}{3.3.happens-before}的关系定义:
    • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。\color{#4285f4}{PS:是JMM对程序员的承诺}
    • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种操作也是被允许的(即:不是按照程序员编写的代码顺序执行的),\color{#4285f4}{PS:这条定义是JMM对编译器和处理器重排序的约束原则}
    \color{red}{3.4.总结}
    • as-if-serial语义的编译器、runtime和处理器为编写单线程程序员创建了一个幻境:单线程程序是按程序的顺序来执行的
    • happens-before语义给编写正确同步的多线程程序的程序员创造一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的

    4.volatile的内存语义

    • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出(其对共享变量所做修改的)消息。
    • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息
    • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
      PS:参照缓存一致性协议:https://www.jianshu.com/p/690b0e4baf61

    4.1.volatile内存语义的实现


    重排序规则表.png
    • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile之后
    • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之后。
    • 当第一个操作是volatile写时,第二个操作是volatile读时,不能重排序。
    JMM采取保守策略,如下
    • 在每个volatile写操作的前面插入一个StoreStore屏障。
    • 在每个volatile写操作的后面插入一个StoreLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadStore屏障。

    5.锁的内存语义

    • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息
    • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出(在释放这个锁之前对共享变量所做修改的)消息
    • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
    //锁释放-获取的示例代码
    class MonitorExample {
      int a = 0;
      public synchronized void writer(){   //1
          a++;                                            //2
      }                                                      //3
      public synchronized void reader(){ //4 
          int i = a;                                      //5
          .......                                            //6
      }
    }
    1)根据程序次序规则,1 happens-before 2, 2 h-b 3,  4 h-b 5, 5 h-b 6
    2)根据监视器锁规则, 3 h-b 4
    3)根据h-b的传递性,  2 h-b 5
    
    //未使用监视器
    class MonitorExample {
      int a = 0;
      public void writer(){   //1
          a++;                                            //2
          Thread.sleep(10);  //睡眠10毫秒,等待reader()的执行
      }                                                      //3
      public void reader(){ //4 
          int i = a;                                      //5
          if(i == 0){                                           //6
            System.out.println("当前i值为" + i + ",当前主内存中a值" + a);
          }                                            
      }
      public static void main(String[] args){
        for(;;){
            MonitorExample me = new MonitorExample();
            Thread t1 = new Thread(() ->{
                me.writer();
            });
            Thread t2 = new Thread(() ->{
                me.reader();
            });
            
            t1.setPriority(10);//优先让writer()方法执行
            t1.start();
            t2.setPriority(1);
            t2.start();
        }
      }
    }
    
    //出现不可见问题:当前i值为0,当前主内存a值为1
    

    6.final域的内存语义

    6.1 final域的重排序规则
    对于final域,编译器和处理器要遵守两个重排序规则

    • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
    • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
      6.2 final语义在处理器中的实现(以X86为例)
    • 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore屏障。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

    synchronized和volatile最终都是使用lock执行

    synchronzied使用lock cmpxchg
    valatile使用lock add$空指令

    CAS

    最终实现:cmpxchg
    多核情况下会多添加lock指令,即 lock cmpxchg

    硬件层面(intel):

    • 确保对内存的读-改-写操作原子执行,带lock前缀的指令在执行期间会使用缓存锁定来保证指令执行的原子性,PS:锁住一个北桥信息(电信号,新版的处理器不采用锁总线的方式)
    • 禁止该指令,与之前和之后的读写指令重排序
    • 把写缓冲区中的所有数据刷新到内存中

    参考书籍:Java并发编程的艺术(方腾飞)

    相关文章

      网友评论

          本文标题:12.3 内存模型

          本文链接:https://www.haomeiwen.com/subject/tauightx.html