volatile使用和Java单例模式中的volatile理解

为了更好理解单例模式中的volatile之前,需要先简单了解下Java的内存模型。

Java的内存模型

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型是什么样子的呢?可以理解为下图的样子:
在这里插入图片描述
解释下:

  • 1、主内存(Main Memory)
    主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。
  • 2、工作内存(Working Memory)
    工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
    线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。原因是直接操作主内存速度较慢。
  • 实例分析
    说了这么多,大家是否很疑惑,为什么要讲这个??? 下面用赋值的代码案例,进行分析,加深理解。
static int s = 0public void test(){
if(s == 0){
s = 3;
System.out.println("初始化成功s=" + s);
}
}

在单线程A执行test方法时。打印输出s的结果

初始化成功s=3

结果和我们所想要的一样,但是如果引入多个线程,输出存在另一种可能:

初始化成功s=3
初始化成功s=3

为什么会出现两条结果呢?结合内存模型理解。工作内存所更新的变量并不会立即同步到主内存,所以虽然线程A在工作内存当中已经把变量s的值更新成3,但是线程B从主内存得到的变量s的值仍然是0,所以输出结果有两条。
在这里插入图片描述
为解决上诉问题的出现,java中引入了volatile关键字。

volatile关键字的使用理解

volatile关键字是一个轻量级的线程同步机制。只可以用来修饰变量,不可以修饰方法以及类。拥有两个特性:

  1. 保证了不同线程对该变量操作的内存可见性.
  2. 禁止了指令重排序.

对上述的可见性和指令重排序做下简单解释:

  • 可见性:
    当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
  • 指令重排序:
    指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。然而,指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果。(如上面所举赋值的代码案例)

单例模式(双检锁方式)中的volatile

先上代码。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  // 1
    if (singleton == null) {  // 2
        synchronized (Singleton.class) {  // 3
        if (singleton == null) {  // 4
            singleton = new Singleton();  // 5
        }  
        }  
    }  
    return singleton;  // 6
    }  
}

很显然上面的代码如果没有使用volidate修饰。CPU为了优化执行效率,可能会出现重排序。从1-2-3 排序为1-3-2。
在多线程下就会出现问题,例如现在有2个线程A,B
线程A在执行第5的代码时,B线程进来,而此时A执行了 1和3,没有执行2,此时B线程判断instance不为null 直接返回一个未初始化的对象,就会出现问题.
而用了volatile,上面的重排序就会在多线程环境中禁止,不会出现上述问题.

volatile的使用场景

使用volatile修饰的变量最好满足以下条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

这里举几个比较经典的场景:

  1. 状态标记量。如下
context = loadContext(); // (1)
inited = true; // (2)
  1. 一次性安全发布.双重检查锁定问题(单例模式的双重检查).
  2. 独立观察.如果系统需要使用最后登录的人员的名字,这个场景就很适合.
  3. 开销较低的“读-写锁”策略.当读操作远远大于写操作,可以结合使用锁和volatile来提升性能.
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值