JVM (三)--内存模型、变量可见性、指令重排、内存屏障

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/FJJ543/article/details/82113161

Java内存模型

Java内存模型(JMM):Java定义的一中抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让Java程序在不同平台下都能达到一致的内存访问效果。

Java内存图示:

1、主内存:线程共享内存,线程读写访问较慢;

包括方法区和Java堆,对于一个共享变量(比如静态变量,堆内存中的实例),主内存存有其“本尊”。

2、工作内存:线程私有的内存,线程访问较快。

对于主内存中的某个变量,使用它的线程的内存空间保存了它的一个”副本”。

线程对共享变量的所有操作都必须在其工作内存中进行,不能直接读写主内存中的变量。

不同线程之间也无法访问彼此的工作内存,变量值在线程之间的传递只能通过主内存来传递。

举个例子:

对于一个静态变量    static int s = 0;

线程A执行代码         s = 3;

扫描二维码关注公众号,回复: 4202584 查看本文章

那么,JMM的工作流程如下图所示:

在上面过程中,线程A把静态变量s=0从主内存中读取到工作内存,再把s=3的更新值写入主内存。

这从单线程的角度来看,完全没有任何问题。

但是如果在程序中引入线程B,线程B执行如下代码:

System.out.println("s="+s);

则会出现2种结果,分别为s=0或者s=3。

出现s=0结果原因:线程A在工作内存中更新的s变量后,不会立即同步到主内存,所以虽然线程A在工作内存当中已经把变量s的值更新成3,但是线程B中从主内存中获取到s的变量值仍然是0,所以输出s=0.

变量可见性

volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile关键字修饰的变量对所有线程的可见性

变量可见性:当一个线程改变了变量的值,新的值会立即同步到主内存中。

当其他线程读取这个变量的时候,也会到主内存中读取到最新值。

volatile具有变量可见性:Java具有先行发生原则(如果一件事发生在另一件事之前,结果必须反映,即是这些事情是乱序的)

注意:这里所谓的事件,实际上就是各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。

上面代码: static int s = 0;

改为:volatile static int s = 0;

线程A先读取主内存中的s=0,线程A在工作内存中修改s=3后,立即将s=3同步到主内存中,这样就保证了线程B读取到的s值是被线程A修改过的了。

volatile关键字虽然能保证变量可见性,但是并不能保证变量的原子性,所以不能保证线程安全。

看下面的例子:

public class VolatileTest {
    public volatile static int count = 0;
    public static void main(String[] args) {
        for(int i = 0 ;i < 10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try{
                        Thread.sleep(1);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    for(int j=0;j<100;j++)
                        count++ ;
                }
            }).start();
        }
        try{
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("count="+count);
    }
}

开启10个线程,每个线程让静态变量count自增100次。

执行结果,可能小于1000:

分析:使用volatile修饰的变量, 为什么在并发自增的时候会出现这样的问题呢?

count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令:

getstatic        //读取静态变量(count)

iconst_1        //定义常量1

iadd               //count增加1

putstatic        //把count结果同步到主内存

虽然线程A执行getstatic的时候,获得的count值都是主内存中的最新值,但是在getstatic到iadd这个过程中,由于不是原子性操作,其他线程在这个过程很可能已经访问主内存中的count并对count的值进行了自增。这样一来,线程A更新的count是没被其他线程更新的陈旧的count值。

适合使用volatile场合:

1)运行结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值。(上面例子)

2)变量不需要与其他的状态变量共同参与不变约束。

例:

volatile static int start = 3;

volatile static int end = 6;

线程A执行    while(start<end){/*do something*/}

线程B执行    start+=3: end+=3;

这种情况下,一旦在线程A的循环中执行了线程B,如果线程B在执行start+=3;后发生阻塞,则有可能使线程A中start==end,从而退出循环。

指令重排

指令重排概念:指令重排是指JVM在编译Java代码,或者CPU在执行字节码的时候,对现有指令进行重新排序。

指令重排的目的:在不改变程序执行结果的前提下,优化程序的运行效率。

!!!注意,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

在某种情况下,指令重排会影响多线程的执行结果。

例:

boolean contextReady = false;

在线程A中执行:

context = loadContext();

contextReady = true;

在线程B中执行:

while(!contextReady){sleep(200);}

doAfterContextReady(context);

以上程序看似没有什么问题。线程B循环等待上下文context的加载,一旦context加载完成,contextReady==true的时候,才执行doAfterContextReady方法。

但是,如果线程A执行的代码发生了指令重排,初始化和contextReady的赋值重排了执行顺序:

boolean contextReady = false;

在线程A中执行:

contextReady = true;

context = loadContext();

在线程B中执行:

while(!contextReady){sleep(200);}

doAfterContextReady(context);

这种情况下,很可能context对象还没加载外,变量contextReady已经为true,线程B直接跳出了循环等待,开始执行doAfterContextReady方法,结果自然会出现错误。

!!!注意,这里Java代码的重排只是为了简单示意,真正的指令重排是在字节码指令的层面。

内存屏障

内存屏障:内存屏障(Memory Barrier)是一种CPU指令,也称为内存栅栏或者栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

内存屏障分为4种类型:

1)LoadLoad屏障:

抽象场景:Load1;LoadLoad;Load2;

Load1和Load2代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

2)StoreStore屏障:

抽象场景:Store1;StoreStore;Store2;

Store1和Store2指令代表两天写入指令。在Store2写入数据 执行前,保证Store1写入数据操作对其他处理器可见。

3)LoadStore屏障:

抽象场景:Load1;LoadStore;Store2;

在Store2写入数据前,保证Load1要读取的数据被读取完毕。

4)StoreLoad屏障:(开销最大)

抽象场景:Store1;StoreLoad;Load2;

在Load2读取操作执行前,保证Store1的写入数据对所有处理器可见。

Volatile在内存屏障种起到的作用

在一个变量被volatile修饰后,JVM会为我们做两件事:

1)在每个volatile变量 写操作前插入StoreStore屏障,每个volatile写操作之后插入StoreLoad屏障;

2)在每个volatile变量 读操作前插入LoadLoad屏障,每个volatile读操作之后插入LoadStore屏障;

还用刚才的例子:

原先代码:

boolean contextReady = false;

在线程A中执行:

context = loadContext();

contextReady = true;

给contextReady 增加volatile修饰符:

volatile boolean contextReady = false;

在线程A中执行:

context = loadContext();

StoreStore屏障

contextReady = true;

StoreLoad屏障

给contextReady增加volatile修饰符后:

在 contextReady = true;这一对有volatile修饰符修饰的contextReady写操作 前加入了StoreStore屏障;

在 contextReady = true;这一对有volatile修饰符修饰的contextReady写操作 后加入了StoreLoad屏障;

这样一来,contextReady = true;前后的指令都不能与其发生指令重排。

volatile关键字特性总结:

1)保证变量在线程之间的可见性。可见性的保证是基于内存屏障指令的;

2)阻止 JVM在编译Java程序时 和 CPU执行字节码文件时 的指令重排;编译时的指令重排遵循内存屏障约束,运行时的指令重排依靠于CPU内存屏障指令来阻止重排。

猜你喜欢

转载自blog.csdn.net/FJJ543/article/details/82113161
今日推荐