java(十三):volatile与内存模型

作为深入理解java中的锁,首先应该掌握volatile的含义和用法。

线程之间的可见性

可见性对于java初学者并不是一个从字面上就可以简单理解的名词。
往深了说,要真正掌握volatile关键字,还需要有基本的jvm的知识,这里只需要理解jvm的分区以及各个区的内容含义就知道了,参考前面的一篇博客jvm内存划分
我们需要掌握的有两点:

  • 同一进程的不同线程之间其内存空间是不共享的,类似虚拟机栈上的局部变量表
  • 除了jvm使用的内存空间,CPU上的寄存器等也可以保存变量(称为CPU缓存,即Cache)

因此,尤其注意在多线程共享一个变量时,需要使用volatile修饰,如下:

volatile boolean flag;

//在线程A执行
public void shutdown(){
    flag = true;
}

//在线程B执行
public void doWork(){
    while(!flag){
        //do work
    }
}

从上述代码中可以得出,当A线程调用后,B线程的函数会立刻结束,因为A线程对flag的改变被直接写入内存,而不是cpu cache,这会导致其他线程重新从内存读取flag变量。而如果不加volatile,那么B线程的循环可能会过一段时间才结束。

指令重排

一旦涉及到多线程,就会出现许多我们无法预测的问题,比如指令重排。
普通的变量会保证以下的执行:

在该方法内,所有依赖赋值结果的地方都能获取到正确的结果,单不能保证变量赋值的顺序和代码中完全一致
比如如下一段代码

//共享变量
int a = 0;
int b = 1;
volatile boolean flag = false;

//A线程执行
public void change(){
    a = 100;
    b = a;
    flag = true;
}

//B线程执行
public void doWork(){
    while(!flag){
        sleep();
    }
    System.out.println(b);
}

在上述代码中,如果不加volatile,可能会出现B输出1。这就是指令重排带来的副作用。
我们前面说过,对于函数内部,即change()函数,肯定会保证a = 100;b = a;的执行顺序,因为存在变量依赖,如果不加volatile,实际上却并不会保证flag = true;在函数change()最后执行,这就是指令重排,即jvm内部对其只进行指令优化后导致语句的执行顺序改变。
于是在多线程,就出现问题了,比如在上述的B线程中,如果没有volatile,则B可能输出1,因为A线程中限制性flag=true,b的值还没来得及更新,B线程就输出b了。

happens-before

先行发生,对于前面的指令重排,并不是所有情况都会存在,java语言内部有一个happens-before原则,它可以判断一个变量的改变在多个线程中是否是可见的。

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

java内存模型中存在一些天然的happens-before关系,但也有些关系不在此中(比如前面的变量赋值),而jvm可以对这些不在此列的进行重排序。

volatile

然后我们来看看volatile关键字,volatile共两个作用:

  • 命令CPU不使用CPU Cache.
    即每次获取变量时,都从内存中去取,以保证变量的最新值,以此来达到变量对于所有线程都是可见的。

  • 禁止该变量的指令重排

实际上volatile的作用就到这里为止了,我们发现volatile并没有实现同步的机制,因此volatile是很轻量级的。一般来说volatile和锁或者synchronized一起使用,已达到同步的目的。
总的来说,volatile变量读操作的性能和普通变量几乎没有什么差别,但是写操作可能会慢一点。

单例举例

volatile最常用的就是计数和单例模式。
下面的例子将说明我们平时写的单例模式是有问题的。

private static class Singleton{

    private static Singleton instance;

    public Singleton(){}

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

上述代码是最常见的创建单例的例子,就实际效果来说,也没太大问题,但问题在于上述的单例模模式并不“单例”,实际上有可能创建多个Singleton对象,问题在于,如果我们承认变量的不可见性,那么如果多个线程调用getInstance(),那么由于不可见性,即使其中一个线程创建了新对象,也存在其他线程没有发现而又创建一次的情况。
最佳的单例模式应该如下:

private static class Singleton{

    private volatile static Singleton instance;

    public Singleton(){}

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

参考博客:Java中Volatile关键字详解happens-before

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/u012882134/article/details/78374474
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞