欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java内存模型(Java Memory Model,JMM)

程序员文章站 2022-03-26 21:03:11
java内存模型java内存模型(Java Memory Model,JMM)简述内存间交互操作关键字volatile原子性可见性有序性先行发生原则java内存模型(Java Memory Model,JMM)简述Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,而每条线程有自己的工作内存,线程的工作内存中保 存了当前线程使用的变量的主内存副本。Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。...

java内存模型(Java Memory Model,JMM)

简述

  1. Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,而每条线程有自己的工作内存,线程的工作内存中保 存了当前线程使用的变量的主内存副本。
  2. Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
  3. 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
  4. java内存模型中的主内存、工作内存与java内存区域中的堆、栈、方法区等并不是同一个层次的划分,这两者基本上没有任何关系。
  5. 如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中是线程私有的。
    java内存模型(Java Memory Model,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操作。

关键字volatile

  1. volatile关键字作用于变量,可以说是Java虚拟机提供的最轻量级的同步机制。
  2. 当一个变量被定义成volatile之后,此变量对所有线程可见,这里的“可见”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点。比如,线程A修改一个普通变量的值但它可能不会立刻向主内存进行回写,另外一条线程B在使用一个普通变量的时候,若线程B的工作内存中有该变量的副本,也不一定会从主内存中再次获取该变量的最新值。
  3. volatile关键字可以让一个变量对所有线程可见的原理是java内存模型定义了volatile关键字的一些特殊规则:对volatile关键字修饰的变量进行了修改后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改;在工作内存中,每次使用volatile关键字修饰的变量前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量所做的修改。
  4. volatile并不能代替锁,它也无法保证一些复合操作的原子性,所以对volatile变量进行非原子操作在并发下也是不安全的。
  5. 使用volatile变量的第二个语义是禁止指令重排序优化,重排序优化是字节码、机器级的优化操作。普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
    举个单例模式中的双重检测例子:
    public class Singleton {
        private static Singleton instance;

        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton(); //!!!
                    }
                }
            }
            return instance;
        }

        public static void main(String[] args) {
            Singleton.getInstance();
        }
    }
上述感叹号标志的代码(实例化一个对象伪代码大致可分为以下三个步骤)
memory = allocate() ;    //①分配对象的内存空间
ctorInstance(memory);   //②初始化对象
instance=memory;        //③设置instance指向刚分配的内存地址

其中②和③的操作是可以进行重排序的,所以在多线程并发的情况下,很有可能会出现某一个线程设置好了instance的内存地址,而还没有调用构造函数进行初始化对象。而另一个线程此时在instance == null对象判空却不成立的情况下,拿到了一个未进行初始化的对象;
最简单的解决办法就给instance加上volatile关键字修饰,从而禁止指令重排序的情况发生!

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的

原子性

  1. 原子性变量操作包括read、load、assign、use、store和write这六个, 我们大致可以认为,基本数据类型的访问、读写都是具备原子性的(例外就是long和double)
  2. 如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性

  1. 可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
  2. volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。所以volatile修饰的变量对于任何一个线程是可见的。
  3. synchronized修饰的同步块或者同步方法也能够实现可见性。因为对一个变量进行unlock操作之前,必须先把此变量同步回主内存中。
  4. final修饰的变量也能够实现可见性。被final修饰的字段在构造器中一旦被初始化完成,那么在其他线程中就能看见final字段的值。

有序性

  1. Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
  2. 上述前半句是指“线程内似表现为串行的语义”),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。比如上述单例双重检测样例中,A线程创建了一个只分配了内存空间而没有初始化的对象。B线程获取到了这个A线程创建的对象,那么在B线程的角度观察A线程就会发现,A线程所有的操作都是无序的。
  3. java语言中提供了volatile关键字来支持线程之间操作的有序性。(volatile关键字本身就包含禁止指令重排序的语义)
  4. java语言中还提供了synchronized关键字来支持线程之间操作的有序性。synchronized则是由“一个变量在同一个时刻只允许一条线程对 其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。因此线程间的操作也就是有序的。

先行发生原则

  1. 先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存*享变量的值、发送了消息、调用了方法等。
i = 1; // 该操作在线程A中执行 

j = i; // 该操作在线程B中执行

i = 2;// 该操作在线程C中执行 
  1. 假设线程A中的操作“i=1”先行发生于线程B的操作“j=i”,那我们就可以确定在线程B的操作执行后,变量j的值一定是等于1,得出这个结论的依据有两个:一是根据先行发生原则,“i=1”的结果可以被观察到;二是线程C还没登场,线程A操作结束之后没有其他线程会修改变量i的值。
  2. 假如C出现在线程A和B的操作之间,但是C与B没有先行发生关系,那j的值会是多少呢?答案是不确定!1和2都有可能,因为线程C对变量i的影响可能会被线程B观察到,也可能不会,这时候线程B就存在读取到过期数据的风险,不具备多线程安全性。

本文地址:https://blog.csdn.net/ab1024249403/article/details/107350531