volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized一般称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,假若能恰当的合理的使用volatile,天然是美事一桩。
为了能比较清晰完全的理解volatile,咱们一步一步来分析。首先来看看以下代码
public class TestVolatile {
boolean status = false;
/**
* 状态切换为true
*/
public void changeStatus(){
status = true;
}
/**
* 若状态为true,则running。
*/
public void run(String t){
if(status){
System.out.println("running...." + t);
}
}
}
上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,能够保证输出"running....."吗?
答案是NO!
这个结论会让人有些疑惑,能够理解。由于假若在单线程模型里,先运行changeStatus方法,再执行run方法,天然是能够正确输出"running...."的;可是在多线程模型中,是无法作这种保证的。由于对于共享变量status来讲,线程A的修改,对于线程B来说,是"不可见"的。也就是说,线程B此时可能没法观测到status已被修改成true。那么什么是可见性呢?
所谓可见性,是指当一条线程修改了共享变量的值,新值对于其余线程来讲是能够当即得知的。很显然,上述的例子中是没有办法作到内存可见性的。
Java内存模型
为何出现这种状况呢,咱们须要先了解一下JMM(java内存模型)
java虚拟机有本身的内存模型(Java Memory Model,JMM),JMM能够屏蔽掉各类硬件和操做系统的内存访问差别,以实现让java程序在各类平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系以下
须要注意的是,JMM是个抽象的内存模型,因此所谓的本地内存,主内存都是抽象概念,并不必定就真实的对应cpu缓存和物理内存。固然若是是出于理解的目的,这样对应起来也无不可。
大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来说,好比咱们上文中的status,线程A将其修改成true这个动做发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B缓存了status的初始值false,此时可能没有观测到status的值被修改了,因此就致使了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式天然就是加锁,可是此处使用synchronized或者Lock这些方式过重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile
volatile具有两种特性,第一就是保证共享变量对全部线程的可见性。将一个共享变量声明为volatile后,会有如下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操做会致使其余线程中的缓存无效。
上面的例子只需将status声明为volatile,便可保证在线程A将其修改成true时,线程B能够马上得知
volatile boolean status = false;
可是须要注意的是,咱们一直在拿volatile和synchronized作对比,仅仅是由于这两个关键字在某些内存语义上有共通之处,volatile并不能彻底替代synchronized,它依然是个轻量级锁,在不少场景下,volatile并不能胜任。看下这个例子:
public class Counter {
public static volatile int num = 0;
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操做
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num++;//自加操做
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
}
针对这个示例,一些同窗可能会以为疑惑,若是用volatile修饰的共享变量能够保证可见性,那么结果不该该是300000么?
问题就出在num++这个操做上,由于num++不是个原子性的操做,而是个复合操做。咱们能够简单讲这个操做理解为由这三步组成:
1.读取
2.加一
3.赋值
因此,在多线程环境下,有可能线程A将num读取到本地内存中,此时其余线程可能已经将num增大了不少,线程A依然对过时的num进行自加,从新写到主存中,最终致使了num的结果不合预期,而是小于30000。
针对num++这类复合类的操做,可使用java并发包中的原子操做类原子操做类是经过循环CAS的方式来保证其原子性的。
public class Counter {
// public static volatile int num = 0;
//使用原子操做类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操做
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,经过循环CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
}
参考:http://www.javashuo.com/article/p-kvwrkeas-cg.htmlhtml