前言:
关于volatile详解,已经烂大街,今年是工作第三年,可我尽然无法清晰地跟别人解释清什么是volatile关键字,为什么要有volatile关键字,以及关键字的原理是什么?如果您看到这里,我也希望您回想一下,能否回答这三个问题,如果不能请继续往下看!
本文从以下三方面进行讲解:
- 为什么需要volatile关键字以及volatile关键字是什么(原理)
- volatile应用场景
- volatile性能怎么样
1、为什么需要volatile关键字
1.1 物理机内存模型
众所周知,由于计算机存储设备与处理器的运算速度有几个数量级差距,现代计算机都不得不加一层读写尽可能接近处理器运算速度的高速缓存(Cache)作为内存与cpu之间的缓存,将运算需要的数据复制到缓存,让运算快速进行;当运算结束后再从缓存同步会内存,这样处理器就无需等待缓慢的内存读写了。但这引起了新的问题:缓存一致性(Cache Coherence)。即多处理器系统,每个处理器都有自己的高速缓存,但又共享同一主内存(Mian Memory),当运算都涉及同一块主内存,那么将谁的数据同步到主内存,以谁的为准呢?为解决一致性问题,物理机处理器都遵循一些协议来解决一致性。
除了高速缓存,为了是处理器内部运算单元尽可能被充分利用,处理器可能对输入的代码乱序执行(Out-Of-Order Execution)优化。与处理器乱序执行优化类似,JVM即时编译也有类似的指令重排序(Instruction Reorder),内存模型如下图:
1.2 Java内存模型
接下来我们看Java内存模型,先上图,其实和物理机内存模型类似
看到这里,各位筒子们可以想一下我们的问题,为什么需要volatile关键字(想一下再看下面内存哦)?
简单来说:引入volatile关键字是为了保证主内存的变量对于不同线程是立即可见的,这样解决了可见性问题,但无法解决高并发。下面说一下volatile是什么?
2. volatile是什么?
关键字volatile是JVM提供的最轻量级的同步机制,当一个变量被定义为volatile时,它将具备两个特性。第一是保证此变量对所有线程的可见性,即一个线程修改变量值,新值对其他线程立即可见(至于原理,本文不进行阐述,有兴趣的童鞋可以翻看周志明老师深入理解JVM第二版P364),而普通变量不能做到这一点。貌似从上面的定义我们可以知道,volatile就能保证多线程再高并发下数据的正确性,因为是立即可见,大家想一下是这样吗?大家可以猜下面一段代码的运行结果:
package com.phil.concurrency.atomicity;
/**
* 功能描述:volatile并发测试
* @auther phil
* @date: 2019/1/6 20:20
*/
public class AtomicityTest {
public static volatile int race=1;
private static final int THREADS_COUNT=20;
public static void increase(){
race++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i=0;i<THREADS_COUNT;i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j=0;j<10000;j++){
increase();
}
}
});
threads[i].start();
}
//java 1.7线程执行完毕剩下主线程和守护线程,深入理解JVM此处为>1
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(race);
}
}
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。读者运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不样,都是一个小于200000的数字,这是为什么呢?
问题就再race++,有经经验的程序员都知道race++是非原子性操作,javap反编译可以看到race++有4条字节码构成,从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存。
所以volatile并不能保证数据安全.
3. volatile使用场景
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性
①、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
②、变量不需要与其他的状态变量共同参与不变约束
而在像如下的代码清单12-3所示的这类场景就很适合使用volatile变量来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来
package com.phil.concurrency.atomicity;
/**
* 功能描述:适合volatile应用场景
* @auther phil
* @date: 2019/1/6 20:36
*/
public class Volatile {
volatile Boolean shutdownRequested;
public void shurdown(){
shutdownRequested = true;
}
public void doWork(){
while (!shutdownRequested) {
//do stuff
}
}
}
好,行文至此,向各位看官提问:
1.什么是volatile关键字
2.为了解决什么问题,有什么特性?
如果还不清楚,建议在读一遍文章,或者去周志明老师<<深入理解JVMP>>(第二版)361寻找答案。我们还有volatile禁止重排序特性未讲解,下一篇讲讲解禁止重排序。
欢迎关注我的公众号:
更多推荐
Java volatile关键字详解
发布评论