Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,好比synchronized、volatile、final、concurren包等。在前一篇文章中,咱们也介绍了synchronized的用法及原理。本文,来分析一下另一个关键字——volatile。算法
本文就围绕volatile展开,主要介绍volatile的用法、volatile的原理,以及volatile是如何提供可见性和有序性保障的等。编程
volatile这个关键字,不只仅在Java语言中有,在不少语言中都有的,并且其用法和语义也都是不尽相同的。尤为在C语言、C++以及Java中,都有volatile关键字。均可以用来声明变量或者对象。下面简单来介绍一下Java语言中的volatile关键字。缓存
volatile的用法性能优化
volatile一般被比喻成"轻量级的synchronized",也是Java并发编程中比较重要的一个关键字。和synchronized不一样,volatile是一个变量修饰符,只能用来修饰变量。没法修饰方法及代码块等。多线程
volatile的用法比较简单,只须要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就能够了。架构
public class Singleton {并发
private volatile static Singleton singleton;分布式
private Singleton (){}微服务
public static Singleton getSingleton() {高并发
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用volatile关键字修饰可能被多个线程同时访问到的singleton。
volatile的原理
在再有人问你Java内存模型是什么,就把这篇文章发给他中咱们曾经介绍过,为了提升处理器的执行速度,在处理器和内存之间增长了多级缓存来提高。可是因为引入了多级缓存,就存在缓存数据不一致问题。
可是,对于volatile变量,当对volatile变量进行写操做的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议
缓存一致性协议:每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。
因此,若是一个变量被volatile所修饰的话,在每次数据变化以后,其值都会被强制刷入主存。而其余处理器的缓存因为遵照了缓存一致性协议,也会把这个变量的值从主存加载到本身的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
volatile与可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。
咱们在再有人问你Java内存模型是什么,就把这篇文章发给他中分析过:Java内存模型规定了全部的变量都存储在主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量的传递均须要本身的工做内存和主存之间进行数据同步进行。因此,就可能出现线程1改了某个变量的值,可是线程2不可见的状况。
前面的关于volatile的原理中介绍过了,Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后能够当即同步到主内存,被其修饰的变量在每次是用以前都从主内存刷新。所以,可使用volatile来保证多线程操做时变量的可见性。
volatile与有序性
有序性即程序执行的顺序按照代码的前后顺序执行。
咱们在再有人问你Java内存模型是什么,就把这篇文章发给他中分析过:除了引入了时间片之外,因为处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,好比load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。
而volatile除了能够保证数据的可见性以外,还有一个强大的功能,那就是他能够禁止指令重排优化等。
普通的变量仅仅会保证在该方法的执行过程当中所依赖的赋值结果的地方都能得到正确的结果,而不能保证变量的赋值操做的顺序与程序代码中的执行顺序一致。
volatile能够禁止指令重排,这就保证了代码的程序会严格按照代码的前后顺序执行。这就保证了有序性。被volatile修饰的变量的操做,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。
volatile与原子性
原子性是指一个操做是不可中断的,要所有执行完成,要不就都不执行。
咱们在Java的并发编程中的多线程问题究竟是怎么回事儿中分析过:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不一样的调度算法进行线程调度。当一个线程得到时间片以后开始执行,在时间片耗尽以后,就会失去CPU使用权。因此在多线程场景下,因为时间片在线程间轮换,就会发生原子性问题。
在上一篇文章中,咱们介绍synchronized的时候,提到过,为了保证原子性,须要经过字节码指令monitorenter和monitorexit,可是volatile和这两个指令之间是没有任何关系的。
因此,volatile是不能保证原子性的。
在如下两个场景中可使用volatile来代替synchronized:
一、运算结果并不依赖变量的当前值,或者可以确保只有单一的线程会修改变量的值。
二、变量不须要与其余状态变量共同参与不变约束。
除以上场景外,都须要使用其余方式来保证原子性,如synchronized或者concurrent包。
咱们来看一下volatile和原子性的例子:
public class Test {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.i);
}
}
以上代码比较简单,就是建立10个线程,而后分别执行1000次i++操做。正常状况下,程序的输出结果应该是10000,可是,屡次执行的结果都小于10000。这其实就是volatile没法知足原子性的缘由。
为何会出现这种状况呢,那就是由于虽然volatile能够保证i在多个线程之间的可见性。可是没法保证i++的原子性。
i++操做,一共有三个步骤:load i ,add i ,save i。在多线程场景中,若是这三个步骤没法按照顺序执行的话,那么就会出现问题。
如上图,两个线程同时执行i++操做,若是容许指令重排,咱们指望的结果是3,可是实际执行结果多是2,甚至多是1。
总结与思考
咱们介绍过了volatile关键字和synchronized关键字。如今咱们知道,synchronized能够保证原子性、有序性和可见性。而volatile却只能保证有序性和可见性。在这里顺便给你们推荐一个架构交流群:617434785,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源。相信对于已经工做和遇到技术瓶颈的码友,在这个群里会有你须要的内容。
那么,咱们再来看一下双重校验锁实现的单例,已经使用了synchronized,为何还须要volatile?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}