九旬老太为何惨死街头 数百头母驴为何半夜惨叫 小卖部安全套为何屡遭黑手 女生宿舍内裤为何频频失窃 连环强奸母猪案究竟是何人所为 老尼姑的门夜夜被敲究竟是人是鬼 数百头母狗意外身亡背后又隐藏着什么 这一切的背后!!到底是人性的扭曲还是道德的沦丧?下面带大家走进Java关键字之——volatile!
在多线程编程中,我们最常用的是synchronized,而对volatile的使用,却对volatile的使用较少。这一方面是因为volatile的使用场景限制,另一方面是因为volatile使用需要更高的技术水平。
volatile关键字好多人都听过,或许也使用过,从字面意思来看很好理解,但是要用好真的不是一件容易的事情。笔者在之前也看过好多大佬讲过volatile,但是仍然没有弄明白。自己在学习的过程中看了许多大神的博客,他们在讲解Java内存模型的时候总是把CPU的内存架构和Java内存模型混为一谈,事实上这俩个完全不是一个概念,CPU的高速缓存并不是主存的一部分,而是CPU本身自带的,就像CPU里面的寄存器一样。或许,他们都有自己的理解!在讲解Volatile关键字之前,必须了解Java虚拟机的内存模型和CUP的内存架构,不了解的同学速度学习前几篇。
Java内存模型:
Android线程篇(五):Java内存模型
CPU内存架构:
Android线程篇(六):CPU内存架构
多线程下的缓存一致性问题:
Android线程篇(七):多线程下的缓存一致性问题
原子操作和指令重:
Android线程篇(八):原子操作和指令重排
volatile翻译过来意思是:不稳定的,易挥发的;
有道是:太极生两仪,两仪生四象,四象生八卦,天地造就万物生灵,既然它存在,必然有它存在的意义,volatile
到底有什么作用,意义何在?
部分摘自“海 子”博客,感谢作者:http://www.cnblogs.com/dolphin0520/p/3920373.html
1.可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
2.有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁unLock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条原则摘自《深入理解Java虚拟机》。
这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
下面我们来解释一下前4条规则:
对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
学习了volatile
的作用之后,我们继续上上篇文章的例子来看:
public int count = 0;
public int TestVolatile(){
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
count++;
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<<<<<"+count);
return count;
}
volatile
具有可见性,给count
加上volatile
就线程安全了,输出的结果就是我们所期望的结果,事实真的如此吗?
public volatile int count = 0;
public int TestVolatile() {
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
increase();
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<<<<<" + count);
return count;
}
public void increase() {
count++;
}
变量count被使用了volatile修饰,那么在thread1中,当count变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将count =2从从主存映射到缓存上,那么在对count进行自增操作以前,会重新到主存中读取count =3,然后自增到count =4,然后写回到主存。上面的过程很完美,但这样是否保证了count最终的结果一定是4呢?
当然,结果要让大家失望了。
我们来分析下为什么?
来看看count++
操作的时候内存都做了什么操作:
1.从主内存里面(栈)读取到count的值,到CPU的高速缓存当中
2.寄存器对count的值进行加一操作
3.将CPU的高速缓存当中的count值刷新到主内存(栈)当中
我们可以看到++这个操作非原子,先读count,然后+1, 最后再写 count
如果变量count被使用了volatile修饰,那么在thread1中,当count变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将count =2从从主存映射到缓存上并且已经做完了自增操作,此时count =3,那么最终主存中count值为3。
所以,如果我们想让count的最终值是4,仅仅保证可见性是不够的,还得保证原子性。也就是对于变量count的自增操作加锁,保证任意一个时刻只有一个线程对count进行自增操作。可以说volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。
具体解决办法请移步:
多线程下的缓存一致性问题:
Android线程篇(七):多线程下的缓存一致性问题
继续来一个例子:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码很典型,很多人都会采用这种标记办法来处理是否进入循环。但是事实上,这段代码会完全运行正确么?不一定,也许在大多数时候是正确的,但是也有可能是错误的(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何会有问题。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰stop
之后就变得不一样了:
//线程1
volatile boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效,也就是执行线程1的CPU缓存中的stop无效。
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。