volatile关键字
- 1.volatile简介:
- 2.三大特性
-
- 1>.保证可见性:
-
- 代码验证:
- 2>.保证有序性(禁止指令重排序)
-
- 有序性的实现原理:
- 3>.不保证原子性:
-
- 解决方式:
1.volatile简介:
volatile 是 JVM 提供的轻量级的同步机制。volatile 关键字可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性。
2.三大特性
1>.保证可见性:
加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。
volatile语义实现原理:
先来看两个与CPU相关的专业术语:
- 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
- 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。
volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
- 写volatile时处理器会将缓存写回到主内存。
- 一个处理器的缓存写回到内存,会导致其他处理器的缓存失效。
代码验证:
例如:
int number = 0;此时number变量是没有可见性的。
volatile int number = 0;前面添加了volatile关键字之后,可以解决可见性问题。
没加volatile关键字之前:
/**
* 普通类:
* 为了验证volatile的可见性
*/
public class Test1 {
int number = 0;
public void add(){
this.number = 10;
}
public static void main(String[] args) {
Test1 test1 = new Test1();
//创建第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test1.number);
try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
test1.add();//暂停3秒后,修改number的值。
System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test1.number);
},"Thread_One").start();
//第二个是main线程
while (test1.number == 0){
//如果第二个main线程 可以监测到number值的改变,就会跳出当前循环,执行后续程序。
}
System.out.println(Thread.currentThread().getName()+"程序结束!");
}
}
运行结果:(程序卡死在 循环while (test1.number == 0)里,跳不出来)
加上volatile关键字之后:
/**
* 变量上加了volatile关键字:
* 为了验证volatile的可见性。
*/
public class Test2 {
volatile int number = 0;
public void add(){
this.number = 10;
}
public static void main(String[] args) {
Test2 test2 = new Test2();
//创建第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test2.number);
try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
test2.add();//暂停3秒后,修改number的值。
System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test2.number);
},"Thread_One").start();
//第二个是main线程
while (test2.number == 0){
//由于变量number上加了volatile关键字,
// 使得第二个main线程可以监测到number值的改变,从而跳出了循环。
}
System.out.println(Thread.currentThread().getName()+"程序运行结束!");
}
}
运行结果:( 循环while (test1.number == 0)可以正常结束)
2>.保证有序性(禁止指令重排序)
简单说明:
计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序,一般分为如下3种:
源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——>内存系统的重排 ——> 最终执行的指令
解释说明:
·单线程环境下,可以确保程序最终执行结果和代码顺序执行结果的一致性(单线程环境下不用关注指令重排,因为是否重排都不会出错)。处理器在进行重排序时,必须要考虑指令之间的数据依赖性。
·多线程环境中,线程交替执行。由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也就无法预测。而用volatile关键字修饰的变量,可以禁止指令重排序,从而避免多线程环境下,程序出现乱序执行的现象。
有序性的实现原理:
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
volatile通过加内存屏障来实现禁止指令重排序。JMM为volatile加内存屏障有以下4种情况:
-
在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
-
在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
-
在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
-
在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
volatile写是在前面和后面分别插入内存屏障,而volatile 读操作是在后面插入两个内存屏障。
内存屏障 | 解释说明 |
---|---|
StoreStore屏障 | 禁止上面的普通写和下面的volatile 写重排序。 |
StoreLoad屏障 | 防止上面的volatile '写与下面可能存在的volatile 读/写重排序。 |
LoadLoad屏障 | 禁止下面所有的普通读操作和上面的volatile读重排序。 |
LoadStore屏障 | 禁止下面所有的普通写操作和上面的volatile读重排序。 |
3>.不保证原子性:
原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的。
所以,volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。
代码验证:
加上volatile关键字 ,不能保证原子性:
/**
* 变量上加了volatile关键字:
* 为了验证volatile的 不保证原子性。
*/
public class Test1 {
volatile int number = 0;
public void add(){
number++;
}
public static void main(String[] args) {
Test1 test1 = new Test1();
//创建10个线程
for (int i = 0;i < 10;i++){
new Thread(() -> {
//每个线程执行1001次+1操作
for (int j = 0;j<100;j++){
test1.add();
}
},"Thread_"+(i+1)).start();
}
//如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
while(Thread.activeCount() >2){
Thread.yield();//礼让其他线程,暂不执行后续程序
}
System.out.println("执行 1000次 +1操作后,number = "+test1.number);
}
}
运行结果:(部分数据可能会丢失)
解决方式:
方式一:方法上加 synchronized 关键字。
方式二:利用AtomicInteger类实现原子性。代码如下:
import java.util.concurrent.atomic.AtomicInteger;
/**
* 变量上加了volatile关键字 ,
* 但 不能保证原子性 的 解决方式。
*/
public class Test2 {
volatile int number = 0;
//解决方式一:方法上加 synchronized 关键字
public void add(){
number++;
}
//解决方式二:如下
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
//每调用一次此方法,加个一。
atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
Test2 test2 = new Test2();
//创建10个线程
for (int i = 0;i < 10;i++){
new Thread(() -> {
//每个线程执行1001次+1操作
for (int j = 0;j<100;j++){
test2.add();//调用不能保证原子性的方法
test2.addMyAtomic();//调用可以保证原子性的方法。
}
},"Thread_"+(i+1)).start();
}
//如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
while(Thread.activeCount() >2){
Thread.yield();//礼让其他线程,暂不执行后续程序
}
System.out.println("执行 1000次 +1操作后,number = "+test2.number);
System.out.println("执行 1000次 +1操作后,atomicInteger = "+test2.atomicInteger);
}
}
运行结果对比: