美文网首页
Java内存模型

Java内存模型

作者: 一个菜鸟JAVA | 来源:发表于2019-11-22 16:47 被阅读0次

    计算机物理内存

    CPU在计算中,不仅仅只有计算,还有对内存中数据的交互.但是因为CPU的计算速度远远大于内存数据读写速度,为了提高CPU的运行效率,不得不在CPU中增加一个高速缓存来解决CPU与内次你速度不匹配的问题.这块内存的空间比较小,无法将所有内存中的数据全部载入高速缓存计算.与此同时也带来了一个缓存一致性问题.为了解决这个问题,引入了一些缓存一致性的协议来解决这个问题.

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

    Java内存模型与jvm内存模型是完全不一样的.Java虚拟机中定义了一种Java内存模型,用来屏蔽各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到一样的内存访问效果.
    Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的细节.这里的变量包括了实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数.因为后者是私有的.
    Java内存模型与硬件的内存模型很像.在Java内存模型中变量都存储在主内存(Main Memory,类比于物理内存)中.每个线程有自己的工作内存(Working Memory,类比于高速缓存),在内部保存着该线程使用的变量的主内存副本拷贝,线程对变量的操作(读取,赋值等)都在工作内存中完成.不同线程之前不能相互读取对方工作内存中的数据,线程之前变量值的传递只能通过主内存.

    线程,工作内存和主内存关系.png

    内存交互操作

    操作 作用位置 作用
    lock(锁定) 主内存变量 把一个变量标识为一个线程独占状态
    unlock(解锁) 主内存变量 把一个变量从锁定状态解除(解除之后才能被其他线程lock)
    read(读取) 主内存变量 把一个变量从主内存输送到工作内存,以后后续load操作使用
    load(载入) 工作内存变量 把read操作从主内存得到的变量放入到工作内存的变量副本中
    use(使用) 工作内存变量 把内存中的变量传递给执行引擎,当字节码指令需要该值时,就会使用该命令
    assign(赋值) 工作内存变量 从执行引擎接收到的值赋值给工作内存变量
    store(存储) 工作内存 把工作内存中的变量传送到主内存中,以便后续write使用
    write(写入) 主内存变量 把从store操作过来的变量的值放入主内存中的变量

    如何从主内存拷贝数据到工作内存,如何从工作内存拷贝到主内存等这些细节如何实现,Java内存模型定义了上面表格中的8种操作来完成.上面的8中操作还不能完全解决主内存和工作内存同步的问题,还需要下面几条规则约束才能完成.

    • 不允许read,load和store,write单独出现.简单点说就是不能从主内存读了数据但工作内存不接受,或者从工作内存写数据但工作内存不接受.
    • 不允许一个线程丢弃最近的assign操作.即变量在工作内存中改变了之后必须写回主内存.
    • 不允许一个线程无原因的(没有发生过assign)将值写回主内存.
    • 一个变量只能在主内存中创建,不允许在工作内存中使用一个未被初始化的变量.
    • 一个变量在同一时刻只允许一个线程对其进行lock操作,但是同一个线程可以执行多次lock操作.在unlock时,必须执行相同次数才能解除lock操作.
    • 如果对一个变量执行lock操作,它会清空该变量在工作空间的值,在执行引擎使用前,需要进行load或者assign操作来初始化变量的值.
    • 如果一个变量没有被一个变量执行lock操作,那么不允许该该线程对它执行unlock操作.同时也不允许去unlock一个被别的线程lock的变量.
    • 对一个变量执行unlock时,必须将工作内存的值写回主内存.即需要执行store和write.

    原子性可见性和有序性

    原子性

    在Java内存模型中,基本数据类型的读写是具有原子性的.但是在Java内存模型中,允许虚拟机在对没有用volatile修饰的64位数据类型划分为两次32位操作来处理.即允许load,store,read和write在处理64位数据时不保证原子性.如果在多线程的情况下,在读取或者写入64位的数据类型值时只成功一半,导致程序异常.但是在现在的商业虚拟机中,很少会出现该情况.具体参照:17.7. Non-Atomic Treatment of double and long
    某些场景下,需要更多大范围的原子性保证,虚拟机提供了lock和unlock来保证,另外还提供了更加高级的monitorenter和monitorexit来保证原子性.这两个关键字反映到Java代码中就是synchronized关键字,所在synchronized之间的代码能保证原子性.

    可见性

    可见性是指一个线程修改一个变量的值,另一个线程能够知道该值被修改.Java内存模型是通过变量值修改后将值写回主内存,读取值时从主内存读取来实现可见性的.无论是volatile修饰的变量还是普通变量.不同的在于使用volatile修饰的变量在读取时能立即从主内存中读取值,写入的时候能立刻同步回主内存.
    除了volatile修饰的变量能实现可见性,另外synchronized和final也能.因为在synchronized执行unlock的时候需要先执行store和write,即将值写回主内存.而finnal修饰的字段因为声明之后无法修改,所以也是具有可见性的.

    有序性

    在Java内存模型中,同一个线程内部观察,所有操作是有序的.但是在另外一个线程内观察,它是无序的.Java语言提供了volatile和synchronized两个关键字来保证程序之间的有序性.volatile本身就提供了禁止指定排序的语义,而synchronized因为只会有一个线程执行lock操作,而持有同一个锁的两个同步块只能串行执行.

    先行发生原则(happens-before)

    上面说的有序性可以通过使用volatile和synchronized来完成,但是如果所有的都通过这种方式来完成,那么将会非常麻烦.所在在Java内存模型中定义了先行发生原则(happens-before),通过它可以来判断数据是否存在竞争,线程是否安全等问题.

    什么是先行发生原则?如果A操作现行与B操作,就是说A操作发生在B操作前面,那么A操作所产生的影响能被B观察到.
    在Java内存模型中已经预先定义了8条规则,这些规则不需要经过特殊编码就已经存在了.如果两个操作不在这些规则中,那么虚拟机可以随意对它们进行重新排序.

    程序次序规则(Program Order Rule):在一个线程内,按照程序书写顺序,书写在前面的操作先行发生于书写在后面的操作.

    i = 1;
    j = i;
    

    同一个线程内,A操作先行发生于B操作,那么最后j的值一定为1.

    管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作.需要注意的是必须是同一个锁.

    class A {
        private Integer number = 0;
        private final Object lockA = new Object();
        private final Object lockB = new Object();
    
        public void method1(){
            synchronized (lockA){
                number = number+1;
            }
        }
    
        public void method2(){
            synchronized (lockA){
                number = number+2;
            }
        }
    
        public void method3(){
            synchronized (lockB){
                number = number+2;
            }
        }
    }
    

    现在创建A类型的实例a.线程t1调用method1,t2调用method2.如果某个时刻t1执行完,接着执行t2,那么在t2中,number的值一定为1,执行完t2之后,number的值一定为3.因为他们公用的是相同的锁.如果换成t2执行method3,那么执行完t2,number的值是无法确定的,因为t1和t2使用的不是用一把锁,所以不满足该条件.

    volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于对这个变量的读操作.

    简单点说就是使用volatile修饰的变量,即使在多线程中,只要有线程修改了volatile的值,那么在其他线程中,修改后的值能立马被读取出来.

    线程启动规则(Thread Start Rule):Thread对象的start()方法先发生于该线程内的每一个动作.
    线程终止规则(Thread Termination Rule):线程中所有操作都先行发生于对此程序的终止诊断.
    对象终结规则(Finalizer Rule):一个对象的初始化函数,构造函数先行发生于它的finalizer方法.
    传递性规则(Transitivity):如果A操作先行发生于操作B,操作B先行发生于操作C,那么就可以得出A先行发生于C操作.

    相关文章

      网友评论

          本文标题:Java内存模型

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