zoukankan      html  css  js  c++  java
  • 第一节: JMM内存模型、CPU缓存一致性原则(MESI)、volatile、指令重排、内存屏障(Memory Barrier)、as-if-serial、happen-before原则

    CPU内部结构划分
    控制单元
    运算单元
    存储单元
    计算机多硬件多CPU结构:
    
    
    

    CPU缓存一致性原则

    JMM同步八种操作介绍:
    (1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
    (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的 变量才可以被其他线程锁定
    (3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中, 以便随后的load动作使用
    (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作 内存的变量副本中
    (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
    (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
    (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
    (8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送 到主内存的变量中
    如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步到主内存中,
    就需要按顺序地执行store和write操作。但Java内 存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。


    JMM三大特性
    原子性
      汇编指令 --原子比较和交换在底层的支持 cmp-chxg
    解决办法: Synchronized Lock锁机制 保证任意时刻只有一个线程访问该代码块。
    public class VolatileAtomicSample {
    
        private static volatile int counter = 0; // volatile无法保证原子性
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        counter++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效
                        //1 load counter 到工作内存
                        //2 add counter 执行自加
                    }
                });
                thread.start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(counter);
        }
    }

    启动10个线程,每个线程执行自增步骤,count++ 是非原子性的。volatile保证数据的可见性,同时存在CPU缓存锁机制以及MESI缓存分布式协议,最后打印的值 <= 10000.

    CPU为了提升性能,会存在指令编排机制。也就会出现内存屏障  见有序性详解。


    可见性 volatile -- LOCK缓存行(有且仅有一个线程会占有缓存行) + CPU缓存一致性原则MESI(独占E-->共享S-->修改M--->其他失效I)
    public class VolatileVisibilitySample {
        private boolean initFlag = false;
        public void refresh(){
            this.initFlag = true; //普通写操作,(volatile写)
            String threadname = Thread.currentThread().getName();
            System.out.println("线程:"+threadname+":修改共享变量initFlag");
        }
    
        public void load(){
            String threadname = Thread.currentThread().getName();
            int i = 0;
            while (!initFlag){
                i++;
            }
            System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
        }
        public static void main(String[] args){
            VolatileVisibilitySample sample = new VolatileVisibilitySample();
            Thread threadA = new Thread(()->{
                sample.refresh();
            },"threadA");
            Thread threadB = new Thread(()->{
                sample.load();
            },"threadB");
            threadB.start();
            try {
                 Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.start();
        }
    }

    分析如下: 只会打印 "线程:threadA:修改共享变量initFlag". 

    修改:

    因为在线程A里面增加了锁机制,同时CPU自身也存在时间片切片,导致线程上下文切换,initFlag会从内存中读取线程B更新的值。

    会把线程B嗅探机制打印出来。打印如下:

    线程:threadA:修改共享变量initFlag

    线程:threadB当前线程嗅探到initFlag的状态的改变25747425

    修改2:使用volatile关键字  ====> JMM缓存一致性原则(MESI) + LOCK缓存行

     


    有序性
    -- 指令重排 ---> 内存屏障(volatile禁止重排优化 )
      查看汇编指令:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp


    as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)。 即在单线程情况下,不能改变程序运行的结果

                 程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

    double p = 3.14; //1
    double r = 1.0; //2
    double area = p * r * r; //3计算面积

    上面例子中1,2存在指令重排操作,但是1,2不能和第三步存在指令重排操作,否则将改变程序运行的结果。

    happen-before原则

    1、 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
    2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)
    3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,
    而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
    4. 线程启动原则 线程的start()方法先于他的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行了start方法之时,线程A对共享变量的修改对线程B可见。
    5. 传递性  A先于B,B先于C,那么A必然先于C
    6. 线程终止原则  线程的所有操作先于线程的终结。Thread.join()方法的作用就是等待当前执行的线程的终止。
    假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
    7. 线程中断规则
       对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    8、对象终结规则 对象的构造函数执行,结束先于finalize()方法


    指令重排发生在什么阶段?
    1. 编译阶段,字节码编译成机器指令码阶段。
    2. CPU运行时,执行指令

    volatile禁止重排优化 ---内存屏障(Memory Barrier)
    下图是JMM针对编译器制定的volatile重排序规则表
    指令重排code示例
    /**
     * 并发场景下存在指令重排
     */
    public class VolatileReOrderSample {
        private static int x = 0, y = 0;
        private static volatile int a = 0, b =0;
        static Object object = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            int i = 0;
    
            for (;;){
                i++;
                x = 0; y = 0;
                a = 0; b = 0;
                Thread t1 = new Thread(new Runnable() {
                    public void run() {
                        //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                        shortWait(10000);
                        a = 1; //是读还是写?store,volatile写
                        //storeload ,读写屏障,不允许volatile写与第二步volatile读发生重排
                        x = b; // 读还是写?读写都有,先读volatile,写普通变量
                        //分两步进行,第一步先volatile读,第二步再普通写
                    }
                }, "t1");
                Thread t2 = new Thread(new Runnable() {
                    public void run() {
                        b = 1;
                        UnsafeInstance.reflectGetUnsafe().storeFence();
                        y = a;
                    }
                });
                t1.start();
                t2.start();
                t1.join();
                t2.join();
    
                /**
                 * cpu或者jit对我们的代码进行了指令重排?
                 * 1,1
                 * 0,1
                 * 1,0
                 * 0,0
                 */
                String result = "第" + i + "次 (" + x + "," + y + ")";
                if(x == 0 && y == 0) {
                    System.err.println(result);
                    break;
                } else {
                    System.out.println(result);
                }
            }
    
        }
    
        public static void shortWait(long interval){
            long start = System.nanoTime();
            long end;
            do{
                end = System.nanoTime();
            }while(start + interval >= end);
        }
    
    }


    如果不要volatile去增加内存屏障?如何解决?
    -- 手动增加屏障,通过Unsafe来解决.
    loadFence() storeFence  fulFence() .
    Unsafe通过BootStwp被加载,否则抛异常。JVM的双亲委派机制
    通过反射来获取。
    public class UnsafeInstance {
    
        public static Unsafe reflectGetUnsafe() {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe) field.get(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    
    
    


    内存屏障 Memory Barrier
    1.写写storestore 2.写读storeload  3.读写loadstore 4.读读loadload 



    volatile禁止重排优化
    volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序 执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实 现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。 
    内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行 顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译 器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器 和CPU,
    不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏 障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出 各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的新版本。总之, volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。
    下面看一 个非常典型的禁止重排优化的例子DCL,如下:
    public class DoubleCheckLock { 
        private static DoubleCheckLock instance; 
        private DoubleCheckLock(){} 
        public static DoubleCheckLock getInstance(){ //第一次检测 
        if (instance==null){ //同步 synchronized (DoubleCheckLock.class) 
           { if (instance == null){ //多线程环境下可能会出现问题的地方 
                  instance = new  DoubleCheckLock();
                 }
              }
          }
        return instance;
      }
    }
    上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题, 但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读 取到的instance不为null时,instance的引用对象可能没有完成初始化。


    总线风暴
    问题:大量使用cas和volatile,会有什么问题? 高并发情况下为什么会产生总线风暴?
    1. CAS ---> CPU工作内存与主内存产生大量的交互
    2. volatile ---> 产生大量的无效的工作内存变量

     解决办法: 

    synchronized 关键字


    单例设计---高并发(加双重锁)
    public class Singleton {
    
        /**
         * 查看汇编指令
         * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
         */
        private volatile static Singleton myinstance;
    
        public static Singleton getInstance() {
            if (myinstance == null) {
                synchronized (Singleton.class) {
                    if (myinstance == null) {
                        myinstance = new Singleton();//对象创建过程,本质可以分文三步
                        //对象延迟初始化
                        //
                    }
                }
            }
            return myinstance;
        }
    
        public static void main(String[] args) {
            Singleton.getInstance();
        }
    }
    
    

    
    
    
     



  • 相关阅读:
    开发win8 metro monogame,显示pubcenter广告时会使游戏卡住的问题的解决方法。
    win8商店应用验证,二进制文件是在调试模式下生成的解决方案。
    slxna,游戏页面切到后台回来后返回sl页面导致sl页面无响应,解决方法。
    支持虚拟化也开来虚拟化就是装不上HyperV的解决方法
    WP中一些耗时的东西
    WP自定义字体
    SystemTray文字颜色问题
    longlistselector 闪烁问题研究
    vs2013安装xna4.0模板
    让textbox紧贴IME
  • 原文地址:https://www.cnblogs.com/qianbing/p/12581736.html
Copyright © 2011-2022 走看看