深入理解Java虚拟机-12.3 Java内存模型- 高飞网

12.3 Java内存模型

2017-02-21 15:45:06.0

    Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

12.3.1 主内存与工作内存

    Java内存模型的主要目标是定义程序中各个变量(实例字段、静态字段和构成数组对象的元素)的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

    Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图所示:

12.3.2 内存间交互操作

    关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的实现细节,Java内存模型定义了以下八种操作来完成:

    lock -> unlock -> read -> load -> use-> assgin -> store -> write

12.3.3 对于volatile型变量的特殊规则

    关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,它将具备两种特性:
    第一是保证此变量对所有线程的可见性——即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不行,变量值在线程间传递均需要通过主内存来完成
    第二是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致

    volatile变量对所有线程是立即可见的,对volatile变量的所有的写操作都能立刻反应到其他线程之中。但volatile变量的运算在并发下却并不一定是安全的。因为运算不一定是原子的。

public class VolatileTest{

    private static volatile int race = 0;

    public static void increase(){
        race++;
    }
    private static final int THREAD_COUNT = 20;

    public static void main(String[] args){
        Thread[] threads = new Thread[THREAD_COUNT];
        for(int i=0;i<THREAD_COUNT;i++){
            threads[i] = new Thread(new Runnable(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        increase();
                    }   
                }
            });
            threads[i].start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(race);
    }
}

    按照书中的解释,++运算是一个非原子操作,因此如果没有锁的话,仍然会有线程安全问题。但读者在jdk7中运行时,总能得到200000。

    由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景吕,仍然要通过加锁来保证原子性:
    □ 运算结果不依赖变量的当前值(像上面的++就依赖),或者能从中确保只有单一的线程修改变量的值。
    □ 变量不需要与其他的状态变量共同参与不变约束。

    另外一个场景就适合,比如当shutdown()方法被调用时,能保证所有线程中执行doWork()方法都能新闻立即停下来。

    选用volatile控制并发的意义:它能让代码比使用其他的同步工具更快,它的同步机制的性能优于锁。

12.3.4 对于long和double型变量的特殊规则

    java内存模型对于long和double有一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。即允许虚拟机实现选择可以不保证64位数据类型原子性。这点就是所谓的long和double的非原子性协定。

    但读取到“半个变量”的情况非常罕见,因为Java内存模型虽然允许对它的非原子,但实现为原子性却是“强烈建议”的。在实际开发中,目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待。因此在编写代码时一般不需要将用到的long和double变量转门声明为volatile。

12.3.5 原子性、可见性与有序性

原子性(Atomicity):大致可认为基本数据类型(long和double除外)的访问读写是具备原子性的。如果需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作,它对应于字节码指令是monitorenter和monitorexit,反映到关键字就是synchronized关键字。

可见性(Visibility):可见性就是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

    除了volatile外,还有synchronized和final关键字可以实现可见性。

有序性(Ordering):volatile可以保证线程内操作的有序性。此外还有synchronized可以。


上一篇:9.2 案例分析
下一篇:12.4 Java与线程