要想更好的理解volatile关键字,咱们先来聊聊基于高速缓存的存储交互:缓存
咱们知道程序中进行计算的变量是存储在内存中的,而处理器的计算速度和内存的读取速度彻底不在一个量级,区别犹如兰博基尼和自行车。安全
要让兰博基尼开一小段就停下来等会自行车显然不太合适,因此在处理器和内存之间加了一个高速缓存,高速缓存速度远高于内存,犹如奔驰,虽然和兰博基尼还有必定差距,每一个处理器都对应一个高速缓存。多线程
当要对一个变量进行计算的时候,先从内存中将该变量的值读取到高速缓存中,再去计算,效率获得明显提高,这是从硬件的的视角描述的内存。ide
Jvm虚拟机从另外一个视角定义的内存模型规定全部变量都存储在主内存中,每一个线程有本身的工做内存,每一个线程的工做内存只能被该线程独占,其它线程不能访问,全部的线程只能经过主内存来共享数据。性能
这里的主内存能够类比于硬件视角的内存,工做内存能够类比于硬件视角的高速缓存。优化
线程执行程序的时候先将主内存中的变量复制到工做内存中进行计算,计算完毕后再将变量同步到主内存中。spa
这么作虽然解决了执行效率的问题,可是同时也带来了其它问题。线程
试想一下,线程A从主内存中复制了一个变量a=3到工做内存,而且对变量a进行了加一操做,a变成了4,此时线程B也从主内存中复制该变量到它本身的工做内存,它获得的a的值仍是3,a的值不一致了。排序
用专业术语来讲就是变量的可见性,此时变量a对于线程来讲变得不可见了。内存
怎么解决这个问题?
volatile关键字闪亮登场:
当一个变量被定义为volatile以后,它对全部的线程就具备了可见性,也就是说当一个线程修改了该变量的值,全部的其它线程均可以当即知道,能够从两个方面来理解这句话:
1.线程对变量进行修改以后,要马上回写到主内存。
2.线程对变量读取的时候,要从主内存中读,而不是工做内存。
可是这并不意味着使用了volatile关键字的变量具备了线程安全性,举个栗子:
public class AddThread implements Runnable {
private volatile int num=0;
@Override
public void run() {
for (int i=1;i<=10000;i++){
num=num+1;
System.out.println(num);
}
}
}
public class VolatileThread {
public static void main(String[] args) {
Thread[] th = new Thread[20];
AddThread addTh = new AddThread();
for(int i=1;i<=20;i++){
th[i] = new Thread(addTh);
th[i].start();
}
}
}
这里咱们建立了20个线程,每一个线程对num进行10000次累加。
按理结果应该是打印1,2,3.。。。。。200000 。
可是结果倒是1,2,3…..x ,x小于200000.
为何会是这样的结果?
咱们仔细分析一下这行代码:num=num+1;
虽然只有一行代码,可是被编译为字节码之后会对应四条指令:
1.Getstatic将num的值从主内存取出到线程的工做内存
2.Iconst_1 和 iadd 将num的值加一
3.Putstatic将结果同步回主内存
在第一步Getstatic将num的值从主内存取出到线程的工做内存由于num加了Volatile关键字,能够保证它的值是正确的,可是在执行第二步的时候其它的线程有可能已经将num的值加大了。在第三步就会将较小的值同步到内存,因而形成了咱们看到的结果。
既然如此,Volatile在什么场合下能够用到呢?
一个变量,若是有多个线程只有一个线程会去修改这个变量,其它线程都只是读取该变量的值就可使用Volatile关键字,为何呢?一个线程修改了该变量,其它线程会马上获取到修改后的值。
由于Volatile的特性能够保证这些线程获取到的都是正确的值,而他们又不会去修改这个变量,不会形成该变量在各个线程中不一致的状况。固然这种场合也能够用synchronized关键字
当运算结果并不依赖变量的当前值的时候该变量也可使用Volatile关键字,上栗子:
public class shutDownThread implements Runnable {
volatile boolean shutDownRequested;
public void shutDown(){
shutDownRequested = true;
}
@Override
public void run() {
while (!shutDownRequested) {
System.out.println("work!");
}
}
}
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Thread[] th = new Thread[10];
shutDownThread t = new shutDownThread();
for(int i=0;i<=9;i++){
th[i] = new Thread(t);
th[i].start();
}
Thread.sleep(2000);
t.shutDown();
}
}
当调用t.shutDown()方法将shutDownRequested的值设置为true之后,由于shutDownRequested 使用了volatile ,全部线程都获取了它的最新值true,while循环的条件“!shutDownRequested”再也不成立,“ System.out.println("work!");”打印work的代码也就中止了执行。
Volatile还能够用来禁止指令重排序。
什么是指令重排序?
Int num1 = 3; 1
Int num2 = 4; 2
Int num3 = num1+num2; 3
在这段代码中cpu在执行的时候会对代码进行优化,以达到更快的执行速度,有可能会交换1处和2处的代码执行的顺序,这就是指令重排序。
指令重排序并非为了执行速度不择手段的任意重排代码顺序,这样必然会乱套,重排序必须遵循必定的规则,1处和2处的代码之间没有任何关系,他们的执行顺序对结果不会照成任何影响,也就是说1->2>3的执行和2->1->3的执行最后结果都为num3=7.咱们说1处和2处的操做没有数据依赖性,没有数据依赖性的代码能够重排序。
再看一下2处和3处的代码,若是把他们交换顺序,结果会不同,为何会不同呢?由于这两处操做都操做了num2这个变量,而且在第二处操做中修改了num2的值。
若是有两个操做操做了同一个变量,而且其中一个为写操做,那么这两个操做就存在数据依赖性,对于有数据依赖性的操做,不能重排序,因此2处和3处的操做不能重排序。
还有一个规则是不管怎么从新排序,单线程的执行结果不能被改变,也就是说在单线程的状况下,咱们是感觉不到重排序带来的影响的。
在多线程的状况下重排序会对程序形成什么影响呢?
举个栗子:
//定义一个布尔型的变量表示是否读取配置文件,初始为未读取
Volatile boolean flag = false; 1
//线程A执行 读取配置文件之后将flag改成true
readConfig(); 2
flag = true; 3
//线程B执行循环检测flag,若是为false表示未读取配置文件,则休眠。若是为true表示已读取配置文件,则执行doSomething()
while(!flag){ 4
sleep(); 5
}
doSomething(); 6
在这段伪代码中若是1处的代码没有用Volatile关键字,可能因为指令重排序的优化,在A线程中,3处的代码 flag=true在2处代码以前执行,致使B线程在配置文件还未读取的状况下去执行相关操做,从而引发错误。
而Volatile关键字能够避免这种状况发生。
他是如何作到的呢?
经过汇编代码能够看出,在3处当咱们对Volatile修饰的变量作赋值操做的时候,多执行了一个指令 “lock add1 $0x0,(%esp)”.
这个指令的做用是使该指令以后的全部操做不能重排序到该指令的前面,专业术语叫作内存屏障。
正是由于内存屏障的存在可以保证代码的正确执行,因此读取Volatile关键字修饰的变量和普通变量没有什么差异,可是作写入操做的时候因为要插入内存屏障,会影响到效率。
实际上在jdk对Synchronized进行优化之后,Synchronized的性能明显提高和Volatile已经差异不大了,Volatile的用法比较复杂,容易出错,Synchronized也能够解决变量可见性的问题,因此一般状况下咱们优先选择Synchronized,可是Synchronized不能禁止指令重排序,貌似这是Volatile的适用场合。