并发编程之——Java内存模式和volatile详解

一、定义

Java内存模型(Java Memory Model,JMM):由Java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性,即屏蔽掉了底层不同计算机的区别。

 

二、缓存一致性

缓存是用来解决CPU执行速率和内存(RAM)数据处理速率不一致而产生的,即CPU执行的速度远超内存处理的速度,这就导致了CPU每次读写内存的数据都会浪费很多时间再等待中,于是便有了CPU缓存,CPU缓存处理速率和CPU很接近,每次先把内存中的速度放入到CPU缓存,从而减少了CPU的等待时间。

多核缓存架构:

 

三、Java内存模型

内存模型:

主内存:

     Java内存模型规定了所有的共享变量都存储在主内存中。

工作内存:

    每个线程私有的内存,线程的工作内存保存了使用到的主内存中共享变量的副本拷贝,线程对共享变量所有的操作都必须在线程的工作内存中进行,不同线程之间无法访问对方的工作内存,线程间变量值的传递需要通过主内存来完成。

内存间的交互操作:

    Java内存模型定义了八种操作来完成主内存与工作内存与线程之间的实现,这八种操作除了lock和unlock外都是原子性的。

    lock(锁定):作用于主内存的共享变量,它把一个共享变量标识为一条线程独占。

    unlock(解锁):作用于主内存的共享变量,它把一个处于锁定状态的共享变量释放出来,释放后的变量才可以被其它线程锁定。

    read(读取):主内存的共享变量,它把一个共享变量的值从主内存传输到线程的工作内存中,以便随后的load操作。

    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的共享变量的值放入到工作内存的变量副本中。

    use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎去执行。

    assign(赋值):作用于工作内存的变量,它把一个从执行引擎收到的值赋给工作内存的变量。

    store(存储):作用于工作内存的变量,它把工作内存中的一个变量值传递到主内存中,以便随后的write操作。

    write(写入):作用于主内存的共享变量,它把store操作从工作内存获取到的变量值写入到主内存的共享变量中。

    当对一个共享变量进行操作时,如当共享变量a = 1;对其进行a = a + 1操作时的内存间的交互操作图如下:

    第一步为read操作,读取共享变量a的值传递到工作内存中,第二步为load操作,把共享变量a的值放入到工作内存的变量副本中,第三步为use操作,交给cpu执行获取值操作(即获取到共享变量a的值),第四步为assign操作,执行a+1后把其结果2赋值到工作内存中之前共享变量a的变量副本中(此时工作内存中变量a的值是2,主内存中变量a的值是1),第五步为store操作,把工作内存中变量a的值传递到主内存中,第六把为write操作,把主内存中共享变量a的值修改为传递过来的值,即把a的值修改为2(此时工作内存和主内存中变量a的值都为2)。

三大特性:

    原子性:该操作是不可分割的,即该操作要么成功要么失败,不存在中间状态,Java内存模型提供了6种原子性的变量操作,即read、load、use、assign、store、write。

    可见性:指当一个线程修改了共享变量的值,其它线程是立刻得知这个修改,如volatile关键字保证了可见性。

    有序性:程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。但是在多线程环境下,有些代码的顺序改变,有可能引发逻辑上的不正确,Java中保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。

 

四、volatile

volatile关键字的语义:

    当一个共享变量被volatile修饰之后,那么它就具备了如下两层语义:

    1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    2.禁止进行指令重排序。

    即volatile关键字保证了有序性和可见性,注意volatile关键字并不保证原子性。

不保证原子性案例说明:

public class Test {

    private static volatile int a = 0;

    public static void main(String[] args) throws InterruptedException {
        for(int k = 0;k < 10;k++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0;i<10000;i++) {
                        a =  a + 1;
                    }
                }
            });
            t.start();
        }
        Thread.sleep(3000);
        System.out.println(a);
    }
    
}

如上代码,开启10个线程对共享变量a共执行10万次加一操作,最终结果都是小于等于10万,即volatile关键字不保证原子性。

可见性案例说明:

public class Test {

    private static boolean isRun = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isRun) {
                    //System.out.println("aa");
                }
                System.out.println(isRun+"结束");
            }
        });
        t1.start();
        Thread.sleep(1000);
        isRun = true;
        System.out.println("测试开始!");
    }
}

    如上代码,变量isRun未加volatile关键字,则会一直处于while循环,说明修改了主内存的变量,并不是立即可见的,如果加上volatile关键字则会在变量被修改后马上结束。

    注意:如果把代码中的注释,即while内的注释去掉,变量isRun不加volatile关键字,也会得到和加volatile关键字一样的效果,这是因为只有在堆变量读取频率很高的情况下,虚拟机才不会及时的回写到主内存中,而当频率没有达到虚拟机认为的高频率时,则和volatile是一样的处理逻辑。

MESI缓存一致性协议:

    多个cpu从主内存读取同一个数据到缓存时,当某个cpu修改了缓存中的该数据时,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到该数据的变化,并且使自己缓存里的数据失效。

volatile关键字实现原理:

    cpu检测到volatile关键字时,会开启MESI缓存一致性协议,即当某个cpu修改被volatile修饰的共享变量时,在store操作时,会对其进行加锁至write操作完成,并且会经过cpu总线通知其它cpu的工作内存中该变量失效,再下次访问时直接从主内存中获取。

volatile原理案例说明:

    案例代码:

public class Test {

    private static volatile int a = 0;

    public static void main(String[] args) throws InterruptedException {
        for(int k = 0;k < 10;k++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0;i<10000;i++) {
                        a =  a + 1;
                    }
                }
            });
            t.start();
        }
        Thread.sleep(3000);
        System.out.println(a);
    }
    
}

    原理图:

    说明:为了简便,省略了一些步骤,案例场景为,10个线程对共享变量a进行a + 1操作,假设线程A和线程B正在同时对变量a进行a+1操作,当线程B先执行到store操作,则cpu检测到该变量被volatile修饰,则对其进行lock操作,线程A此时则处理等待,线程B再进行最后的write操作时经过消息总线,其它的cpu监听到该情况,则会把当前工作内存中的共享变量a的值置位失效(即本次的修改作废),当下次再来获取时,直接从主内存中获取到最新的值,而此时write操作已经修改完成,最后进行unlock操作,所以变量a的值最终都会小于等于10万。

五、volatile应用场景

  1. volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大,并且非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
  2. volatile可以禁止重排序。(单例的双重检查中有应用,请参考本人设计模式专栏,单例模式。)
发布了61 篇原创文章 · 获赞 81 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/m0_37914588/article/details/104039302