zoukankan      html  css  js  c++  java
  • 秋招之路6:java(线程)内存模型JMM

    计算机的缓存模型

    解决问题
    cpu缓存是为了减少处理器访问内存所需平均时间的部件。
    在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。
    其容量远小于内存,但交换速度却比内存快得多。

    步骤
    当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。
    如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

    原理
    缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。
    这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。
    有效利用这种局部性,缓存可以达到极高的命中率。

    java(线程)内存模型

    java 线程内存模型跟cpu缓存模型非常相似,是基于cpu缓存模型来建立的,
    并且java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
    需要注意的是,线程i是直接和他的工作内存进行通信的 工作内存在初始化的时候,将主内存的各个共享变量加载到工作内存

    各种常用的原子操作

    + lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
    
    + read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
    
    + load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
    
    + use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
    
    + assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
    
    + store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
    
    + write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
    
    + unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
    

    下面是一个典型的volalite的执行过程

    多处理器环境下,缓存不一致问题的两种解决方法

    总线加锁(早期,性能低):cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他的cpu就没法去读或者去写这个数据,
    直到这个cpu使用完数据释放锁后,其他cpu才能进行读取。
    MESI缓存一致性协议:多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据的时候,
    该数据会马上同步到主内存,其他cpu会通过总线嗅探机制(jian ting)感知数据的变化,
    从而设置自己的缓存里的数据为失效状态。

    JMM解决缓存不一致:volatile底层实现原理

    底层主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存并写回到主内存

    IA-32架构软件开发手册对lock指令的解释:
    1.会将当前处理器缓存行的数据立即写回到系统内存。
    2.这个写回内存的操作会引起在其他cpu里面缓存了该内存地址的数据无效。
    如果在lock指令执行的时候,有其他的读操作,或者写操作,会出现不一致,又如何解决?
    加上一把非常细力度的锁

    就是说:volatile 是轻量级的锁,它不会引起线程上下文的切换和调度。
    足够轻量,只是锁住一条内存赋值指令。

    volatile的特点

    volatile可以保证可见性,但是不能保证原子性

    可见性:对一个volatile的读,总是可以看到对这个变量最终的写.
    (并非是修改了之后,会"通知"其他线程去取最新的数据,而是下一次有线程取这个数据的时候,
    禁止他们从自己的线程缓存中取数据,直接到原始的内存去取最新值。)

    原子性:volatile对单个读 / 写具有原子性(32 位 Long、Double),但是复合操作除外,例如i++ 。

    具体来讲一个这样的场景:有两个线程一起做i++的操作,由于volatile的实现原理,
    其中一个在执行lock指令的时候,会使得其他cpu里面缓存了该内存地址的数据失效。(但是其他的cpu可能已经做完了这个i++操作)
    这样不就让其他cpu做的操作失效了嘛。 如何解决呢? 使用synchronize.

    有序性:JVM 底层采用“内存屏障”来实现 volatile 语义,防止指令重排序。

    指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
    “内存屏障”:简单理解:有lock之后,cpu就不会把后面的语句优化到前面;前面的语句优化到后面。

    volalite的常见的两个场景

    第一个场景:状态标记变量(保证可见性)

    public class Synch
    {
        volatile boolean go = true;
        public static void main(String[] args) throws InterruptedException{
            new Synch().test();
        }
        public void test() throws InterruptedException{
            new Thread(){
                public void run(){
                    System.out.println(">>>t1 begin starting");
                    while(go == true){
                    }
                    System.out.println(">>>OK");
                }
            }.start();
    
            Thread.sleep(2000);
    
            new Thread(){
                public void run(){
                    System.out.println(">>>t2 begin starting");
                    go = false;
                    System.out.println(">>>t2 begin stop");
                }
            }.start();
        }
    }
    

    上面的这边代码就是一个典型的状态标记变量

    如果去掉volatile,输出为
    t1 begin starting
    t2 begin starting
    t2 begin stop
    因为各自线程使用各自线程的工作内存,线程1的工作内存一直是true;

    加上volatile以后,线程2执行go的修改时候,
    会将当前处理器缓存行的数据go 立即写回到系统内存,并且立刻进行通知。
    所以输出为:
    t1 begin starting
    t2 begin starting
    t2 begin stop
    OK

    如果去掉Thread.sleep(2000);那么结果输出ok
    因为jvm会进行指令的优化重排,使得go = false;总是先于while(go == true)执行。

    第二个场景:double check
    由来:
    单例模式的创建有两种方案:
    饿汉模式:无论是否调用该方法,在单例类被加载的时候进行单例的创建。
    懒汉模式:只有当第一次请求单例实例时候才会进行创建。
    在使用 懒汉模式实现单例时,经常被使用的一种方法是:双重检查锁定的写法。

    public  class DoubleCheckedLocking{
      private static Instance instance;                 
     
      public static Instance getInstance(){             
        if(instance ==null){                            
          synchronized (DoubleCheckedLocking.class){    
            if(instance ==null)                         
              instance=new Instance();                  //1 
          }
        }
        return instance;
      }
    

    为何要使用双重检查锁定呢?
    需要注意的是:正确的双重检查锁定模式创建单例,需要需要使用 volatile。

    private static volatile  Instance instance;  
    

    因为对象创建的三个步骤:
    对象的创建可以归为如下三个步骤:

    1.分配内存对象。
    2.调用构造方法,执行初始化。
    3.将对象引用赋值给变量。
    多线程中:在实际运行中,以上操作中 2 ,3 指令 会发生 指令重排。

    以下为具体情形,仍然以A、B两个线程为例:

    1. A、B线程同时进入了第一个if判断
    2. A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
    3. 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
    4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
    5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
  • 相关阅读:
    Outdated Kotlin Runtime
    安装 intelliJ idea 。 快速学会kotlin
    配置lambda
    kotlin 安装 使用
    android stuidio 导入项目问题。
    下载 ....aar jitpack.io 打不开。
    android studio 汉化包 美化包
    安卓架构 视频
    剑指offer-把二叉树打印成多行
    剑指offer-整数中1出现的次数(从1到n整数中1出现的次数)
  • 原文地址:https://www.cnblogs.com/whyaza/p/12329483.html
Copyright © 2011-2022 走看看