文章目录
- 什么是volatile ?
- volatile三大特性
- volatile如何使用
- volatile保证可见性
- volatile不保证原子性
- volatile禁止指令重排
- volatile总结
什么是volatile ?
- volatile是一个Java关键字
- volatile是Java虚拟机提供的轻量级的同步机制
volatile三大特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
volatile如何使用
在学习如何使用volatile关键字之前我们需要对Java内存模型(JMM)有一定了解
不熟悉的小伙伴可以看这篇文章:Java内存模型(JMM)详解!
在学习JMM的时候我们提到JMM的三个问题,分别是原子性、可见性和有序性 ,而可见性和有序性是可以通过volatile关键字来解决的,这也验证我们volatile三大特性,保证可见性、不保证原子性、禁止指令重排其实就是保证有序性。
volatile保证可见性
public class JMMDemo {
// 加 volatile 可以保证可见性
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
这段代码我们在主线程新建一个线程执行,我们在主线程将num的值由0改为1如果不加volatile 在学习JMM的时候我们知道主线程对num的修改 线程1是感知不道的。那么程序就会陷入死循环永远无法执行结束。
Volatile做了啥?
volatle保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
volatile如何保证可见性
线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;
其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。
聊一下MESI(缓存一致性协议)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
通过嗅探发现数据是否失效
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
嗅探的缺点
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
volatile不保证原子性
public class VDemo02 {
// volatile 不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
这段代码我们常见20个线程每个线程对num执行1000次加1操作,理论上num结果应该为 2 万。运行会发现num的值小于2万。所以说volatile并不能保证原子性操作。
关于如何保证原子性可以使用synchronized、 lock、原子类。这里就不解释了。
volatile禁止指令重排
什么是指令重排?
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?
JMM对底层尽量减少约束,使其能够发挥自身优势。
因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
一般重排序可以分为如下三种:
-
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
-
指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
as-if-serial
-
不管怎么重排序,单线程下的执行结果不能被改变。
-
编译器、runtime和处理器都必须遵守as-if-serial语义。
那Volatile是怎么保证不会被执行重排序的呢?
内存屏障
java编译器会在生成指令系列时在适当的位置会插入内存屏障
指令来禁止特定类型的处理器重排序。
为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
写
读
上面的我提过重排序原则,为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。
如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。
从JDK5开始,提出了happens-before
的概念,通过这个概念来阐述操作之间的内存可见性。
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
如果现在我的变了flag变成了false,那么后面的那个操作,一定要知道我变了。
聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法。
atile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。`
如果现在我的变了flag变成了false,那么后面的那个操作,一定要知道我变了。
聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法。
volatile总结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。
- volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
- volatile可以使得long和double的赋值是原子的。
- volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。