zoukankan      html  css  js  c++  java
  • JVM内存模型

    概要:Java引入内存模型是为了在各种硬件和OS下达到一致的内存访问效果,但它的引入也带来了并发下的一致性问题(因为主内存共享)。因此主要内容:一是内存模型的划分,二是解决并发(多线程)环境下操作的一致性问题,包括原子性、可见性、有序性。

    1、Java内存模型

    内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。 

    Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(C/C++等则直接使用物理机和OS的内存模型,使得程序须针对特定平台编写),它在多线程的情况下尤其重要。

    JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量是指共享变量,存在竞争问题的变量,如实例字段、静态字段、数组对象元素等,不包括线程私有的局部变量、方法参数等,因为私有变量不存在竞争问题。可以认为JMM包括内存划分、变量访问操作与规则两部分。

    1.1、内存划分

    物理机中的内存模型:

    Java内存划分如下所示(可与上述物理机中的内存模型作类比):

     

    分为主内存和工作内存,每个线程都有自己的工作内存,它们共享主内存。

    主内存(Main Memory)存储所有共享变量的值。

    工作内存(Working Memory)存储该线程使用到的共享变量在主内存的的值的副本拷贝。

    • 线程对共享变量的所有读写操作都在自己的工作内存中进行,不能直接读写主内存中的变量(volatile变量也不例外,虽然它看起来如同直接访问主内存一般)。
    • 不同线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存完成。

    注:这种划分与Java内存区域中堆、栈、方法区等的划分是不同层次的划分,两者基本没有关系。硬要联系的话,大致上主内存对应Java堆中对象的实例数据部分、工作内存对应栈的部分区域;从更低层次上说,主内存对应物理硬件内存、工作内存对应寄存器和高速缓存。

    1.2、内存间交互的操作和规则

    (这里介绍的访问操作及规则完全确定了Java程序中哪些内存访问在并发下是安全的,1.3节介绍与此等效的判断原则——先行发生原则)

    1.2.1、8个原子操作和执行规则

    8个原子操作:JMM定义了8个原子操作来完成工作内存和主内存间的交互:

    1、lock(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
    2、unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的load动作使用。
    4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    5、use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
    6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    7、store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的write操作使用。
    8、write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。

    8个原子操作的执行规则:JMM还规定了执行上述8中基本操作时需满足如下规则:

    1、不允许read和load、store和write操作之一单独出现,即不允许一变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况。即要求read、load成对顺序出现,但不要求连续出现(中间可以插入其他指令),store、write亦然
    2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
    3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
    4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
    5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
    6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
    7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
    8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

    从上面可以看出,把变量从主内存复制到工作内存需要顺序执行read、load,从工作内存同步回主内存则需要顺序执行store、write。总结:

    read、load、use必须成对顺序出现,但不要求连续出现。assign、store、write同之;

    变量诞生和初始化:变量只能从主内存“诞生”,且须先初始化后才能使用,即在use/store前须先load/assign;

    lock一个变量后会清空工作内存中该变量的值,使用前须先初始化;unlock前须将变量同步回主内存;

    一个变量同一时刻只能被一线程lock,lock几次就须unlock几次;未被lock的变量不允许被执行unlock,一个线程不能去unlock其他线程lock的变量。

    1.2.2、volatile变量访问规则

     volatile变量是JVM提供的最轻量级的同步机制。Java内存模型对volatile专门定义了一些特殊的访问规则,被volatile修饰的变量可以保证可见性、有序性,但不保证原子性。

    1、保证可见性:即一个线程修改了一变量的值,其他线程立即可见该变量的新值。

    原理:一个线程对变量修改后立即同步回主内存,即 assign、store、write须连续执行、其他线程对变量读取前立即从主内存刷新新值到工作内存即 read、load、use须连续执行

    示例:用volatile变量控制并发:

     1     volatile boolean shuDownFlag;
     2 
     3     public void showDown() {
     4         shuDownFlag = true;
     5     }
     6 
     7     public void doWork() {
     8         while (!shuDownFlag) {
     9             // do stuff
    10         }
    11     }
    View Code

    2、保证有序性:禁止指令重排序。

    原理:确保指令重排序时不会把后面指令排列到内存屏障(Memory Barrier,也称内存栅栏:在volatile变量上加lock前缀指令)之前的位置,并且不会把变量之前的指令排到内存屏障之后的位置,即在执行内存屏障这句话时,这前面的操作已全部完成。

    示例:

    Java单例常用DCL(双锁检测)来实现,单例变量用volatile修饰:

    public class DoubleCheckLock {
    
        private static volatile DoubleCheckLock instance;
    
        private DoubleCheckLock(){}
    
        public static DoubleCheckLock getInstance(){
    
            //第一次检测
            if (instance==null){
                //同步
                synchronized (DoubleCheckLock.class){
                    if (instance == null){
                        //多线程环境下可能会出现问题的地方
                        instance = new DoubleCheckLock();// 分为 分配空间、初始化对象、引用赋值 三步,若没有volatile则后两步可能重排序从而出错
                    }
                }
            }
            return instance;
        }
    }
    View Code

    用变量标识配置文件是否加载完成,若没有volatile修饰,变量则可能被提前置true,导致其他线程提前开始使用配置信息,而此时实际上还没加载好。代码示例:

    class MixedOrder{
        int a = 0;
        boolean flag = false;
        public void writer(){
            a = 1;
            flag = true;
        }
    
        public void read(){
            if(flag){
                int i = a + 1;
            }
        }
    }
    View Code

    3、不保证原子性

    示例:

     1     private static volatile int race = 0;
     2 
     3     public static void increase() {
     4         race++;
     5     }
     6 
     7     public static void main(String[] args) {
     8         int threadNum = 10;
     9         int countRange = 1000;
    10         Thread[] threads = new Thread[threadNum];
    11         for (int i = 0; i < threadNum; i++) {
    12             threads[i] = new Thread(new Runnable() {
    13 
    14                 @Override
    15                 public void run() {
    16                     // TODO Auto-generated method stub
    17                     for (int i = 0; i < countRange; i++) {
    18                         increase();
    19                     }
    20                 }
    21             });
    22             threads[i].start();
    23         }
    24         while (Thread.activeCount() > 1) {
    25             Thread.yield();
    26         }
    27         System.out.println(race);// 结果不是threadNum*countRange,说明volatile变量并没保证原子性。原因也很明了:race++不是原子操作,虽然线程拿到race值时是最新的,但执行完加一操作写回时可能race值已经被其他线程更新了,导致更新被覆盖。
    28     }
    View Code

    输出值不等于threadNum*countRange,说明volatile变量并没保证原子性。原因也很明了:race++不是原子操作,虽然线程拿到race值时是最新的,但执行完加一操作写回时可能race值已经被其他线程更新了,导致更新被覆盖。

     

    指令重排序

    含义:只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致。如java语言规范就是规定JVM线程内部维持顺序化语义。

    类型:计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

    编译器优化的重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

    指令并行的重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

    内存系统的重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

    其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题

    指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当地对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

    1.2.3、long和double型变量的特殊规则(非原子性协定)

    Java内存模型要求前述8个操作具有原子性,但对于64位的数据类型long和double,在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。即未被volatile修饰时线程对其的读取不是原子操作,可能只读到“半个变量”值。虽然如此,商用虚拟机几乎都把64位数据的读写实现为原子操作,因此我们可以忽略这个问题

    1.3、先行发生原则

    先行发生(happens-before)原则用来确定一个访问在并发环境下是否安全、数据是否存在竞争。其与1.2节介绍的访问操作及规则等效

    Java内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行次序不符合先行原则且无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

    这些规则包括:

    1、程序次序规则(Program Order Rule):一个线程内,按照代码顺序(虑到分支、循环等结构,准确地说应该是控制流顺序),书写在前面的操作先行发生于书写在后面的操作。
    2、锁定规则(Monitor Lock Rule):一个unLock操作先行发生于后面对同一个锁的lock操作。“后面”指时间上的先后顺序。
    3、volatile变量规则(Volatile Variable Rule):对一个变量的写操作先行发生于后面对这个变量的读操作。“后面”指时间上的先后顺序。
    4、传递规则(Transitivity):如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
    5、线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个一个动作。
    6、线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(通过Thread.interrupted()检测)。
    7、线程终止规则(Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
    8、对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。

    注:先行发生与时间先后顺序之间没有必然联系,衡量并发安全问题必须以先行发生原则为准。如对于一个普通变量的不带同步的get、set方法,让时间上线程1、2先后调用get、set方法,线程2并不一定得到线程1设置的值。因为它们不符合先行原则也不能由之导出。

    3、原子性、可见性、有序性

    JMM是围绕着在并发过程中(多线程环境下)如何处理原子性、可见性、有序性这3个特征来建立的。

    总结:

    JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可见性及其有序性。

    3.1、原子性

    含义:是指对变量的某操作在同一时刻只能被一个线程执行,只有该线程执行完了其他线程才能执行该操作。该操作可以只有一个步骤也可以有多个步骤,后者可以称为组合操作。

    JMM实现:

    由JMM直接保证的原子性变量操作包括read、load、use、assign、store、write;基本数据类型的读写也是原子性的(long、double也可以当做原子性)。

    由JMM的lock、unlock可实现更大范围的原子性保证,虽用户没法直接用之,但可用synchronized关键字来保证原子性。synchronized关键字编译后在同步块前后形成字节码指令monitorenter、monitorexit,这两个指令最终即利用了lock、unlock操作保证其间组合操作的原子性。

    3.2、可见性

    含义:是指当一个线程修改了共享变量的值,其他线程立即得知该修改。

    JMM实现:

    (volatile)变量值被一个线程修改后会立即同步回主内存、变量值被其他线程读取前立即从主内存刷新值到工作内存。即read、load、use三者连续顺序执行,assign、store、write连续顺序执行

    (synchronized)1.2.1节中原子操作执行规则8——“对一个变量执行unlock操作之前,必须先把此变量同步回主内存,即执行store、write”。

    (final)final修饰的字段在构造器中一旦初始化完成,且构造器没有把“this”的引用传递出去,则其他线程可立即看到final字段的值。

    3.3、有序性

    含义:在一个线程内所有的操作都是有序的,表现为串行化语义(在一个线程观察另一个则操作不一定有序,是因为有“指令重排序”现象和“工作内存与主内存同步延迟”现象)。

    JMM实现:

    (volatile)禁止指令重排序

    (synchronized)1.2.1节中原子操作执行规则5——“一个变量在同一个时刻只允许一条线程对其执行lock操作”,此规则决定了持有同一个锁的两个同步块只能串行进入。

    4、参考资料

    [1]《深入理解Java虚拟机——JVM高级特性与最佳实践》

  • 相关阅读:
    软件开发者路线图 从学徒到高手 读后感
    Greplin:搜索你的所有在线社交内容
    黄小琥 / 简单/不简单 / 没那么简单
    讯雷 云系列 不错
    来自Amazon、Cloudera、Microsoft与IBM的HadoopasaService
    FAQs and feedback
    I've Never Been To Me by Charlene with lyrics
    烧香网 历害
    4A公司
    一个新的比较网站,做的不错
  • 原文地址:https://www.cnblogs.com/z-sm/p/7196347.html
Copyright © 2011-2022 走看看