什么是volatile关键字?

在讲解volatile关键字之前,我们有必要了解一下CPU,高速缓存和内存的关系,以及java的内存模型。

  1. CPU,高速缓存和内存
    CPU为了更快的执行代码,于是当从内存中读取数据时,并不是只读取自己想要的部分,而是读取足够的字节来填入高速缓存行,当CPU访问相邻的数据时,就不必每次都从内存中读取,而是从高速缓存中读取,提高了效率。因为访问内存要比访问高速缓存用的时间多得多

  2. Java内存模型
    java内存模型简称JMM,是java虚拟机定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各个平台下都能达到一致的内存访问效果。
    java内存模型如下:
    在这里插入图片描述
    有几个概念需要注意:
    1,主内存
    主内存可以简单理解为计算机当中的内存,但又不完全等同,主内存被所有的线程所共享,对于一个共享变量(静态变量,或者堆内存中的实例)来说,主内存当中存储了它的“本尊”。
    2,工作内存
    工作内存可以简单理解为计算机当中的CPU高速缓存,每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量(因为访问内存的效率很低),不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行

举个例子:
对于一个静态变量:static int s = 0;
线程A执行如下代码:s = 3;
那么java内存模型的工作流程如下:
在这里插入图片描述
在这里插入图片描述
通过一系列内存读写操作指令,线程A把静态变量s = 0从内存读到工作内存,再把s = 3的更新结果同步到主内存当中。从单线程的角度没问题,当我们引入线程B,执行如下代码:
System.out.println(“s=” + s); 结果可能是s = 3,也有可能是s = 0;
情况如下:
在这里插入图片描述
在这里插入图片描述
因为工作内存所更新的变量并不会立即同步到主内存,所以虽然线程A在工作内存当中已经把变量s更新成3,但是线程B从主内存读取的s仍然是0,从而输出s = 0。

synchronized虽然保证线程安全,但是对程序性能的影响太大,有一种轻量级的解决办法,也就是我们今天的主角volatile。
volatile关键字最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性。
可见性:当一个线程修改了变量的值,新的值会立即同步到主内存当中,而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
为什么volatile关键字可以有这样的特性,这得益于java语言的先行发生原则。对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。
当静态变量s之前加上volatile修饰符,线程B的输出一定是s = 3;

那么volatile能保证线程安全吗?看下面的代码:

public class VolatileTest {
    private static volatile 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(10);
                    } 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);
    }
}

输出结果为:

770

使用volatile修饰的变量为什么并发自增的时候会出现这样的问题呢?这是因为count++这一行代码本身并不是原子操作,在字节码层面可以拆分成如下指令:

getstatic        //读取静态变量(count)
iconst_1        //定义常量1
iadd               //count增加1
putstatic        //把count结果同步到主内存

虽然每一次执行getstatic的时候获取的都是主内存中最新的变量值,但是进行iadd的时候,由于并不是原子操作,其他线程在这个过程中很可能让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,start可能先更新成6,造成了一瞬间start == end,从而跳出while循环的可能性。

什么是指令重排?

指令重排是指JVM在编译java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。这里所说的不改变执行结果,是指不改变单线程下的程序执行结果。然而指令重排序在某些情况下会影响到多线程的执行结果。
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方法,结果自然会出现错误。

解决指令重排的法宝就是内存屏障。

什么是内存屏障?

内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使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的写入对所有处理器可见。StoreLoad屏障的开销最大。

volatile做了什么?
在一个变量被volatile修饰之后,JVM为我们做了2件事:
1,在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
2,在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
或许这样说有点抽象,我们看刚才的代码:

boolean contextReady = false;
在线程A中执行:
context = loadContext();
contextReady = true;

我们加了volatile之后就变成如下效果:
在这里插入图片描述
由于加入了StoreStore屏障,屏障上方的普通写入语句和屏幕下方的写入语句无法交换顺序,从而成功阻止了指令重排序。
在这里插入图片描述

volatile特性总结:

1,volatile保证变量在线程之间的可见性,可见性的保证是基于CPU的内存屏障指令,被称为JSR-133抽象为happens-before原则(先发原则)。
2,volatile阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障约束,运行时依靠CPU屏障指令来阻止重排。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值