1.volatile概述
volatile是一个关键字,它能保证变量在多线程之间的可见性,禁止CPU执行时进行指令重排操作(内存屏障)从而能保证有序执性,但是它并不能保证原子性。
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。
lock前缀指令相当于一个内存屏障(也称为内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时,不会把后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;也即是,在执行到内存屏障这个指令时,在他前面的操作已经完成。
(2)它会强制将缓存的修改操作立即写入主存。
(3)如果是写操作,他会导致其他CPU中对应的缓存无效。
所以
2.volatile的可见性
volatile的功能就是,将被修改的变量,在被修改后可以立即同步到主内存,被修改的变量在每次被使用之前都从主内存刷新,其实本质也是通过内存屏障来实现可见性。
写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写会主存,读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于store buffer和invalidate queue的非实时性带来的问题。
代码:
public class VolatileDemo {
//定义一个共享变量
private static boolean flag;
public static void main(String[] args) {
System.out.println("开启main线程。。。");
Thread t1 = new Thread(() -> {
while (!flag) {
//System.out.println("值未改变,当前值为:" + flag);
}
System.out.println("线程:" + Thread.currentThread().getName() + "感知到了flag值的变化,当前值为:" + flag);
}, "t1");
t1.start();
//主线程睡眠100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
flag = true;
System.out.println("线程:" + Thread.currentThread().getName() + "将值改为:true");
}, "t2");
t2.start();
}
}
上面代码很简单 定义一个共享变量(此时没有加volatile修饰),开启两个线程,一个线程修改变量的值,另外一个线程一直去循环获取修改后的值。
执行结果:
当我执行这段代码的时候就会发现,程序一直在执行没有结束,线程 T1一直没有获取到线程T2修改后的值。(其实一直等下去线程T1也是能获取到最新的flag值,不过不知道要等到猴年马月,我没有继续等哈哈)
加上volatile关键字之后再执行刚才的代码。
在让我们看一下执行结果
很显然T2将 flag值 false--->true,T1立即就获取到了更新后的值。
2.1总结
通过上述例子,我们会发现当变量没有被volatile 关键字修饰的时候,多线程对变量的操作结果很难被其他线程发现。当变量被volatile关键字修饰以后,多线程操作变量的结果会瞬间被其他线程发现,这也就是本节第二个知识点:volatile支持可见性。
3.volatile禁止指令重排
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
硬件层的内存屏障
Intel硬件提供了一系列的内存屏障,主要有:
1. lfence,是一种Load Barrier 读屏障
2. sfence, 是一种Store Barrier 写屏障
3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:
屏障类型 | 指令示例 | 说明 |
LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到 主内存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作 才能执行 |
更直观的可以用图形来看:
3.1总结
volatile只能保证可见性和有序性但不能保证原子性,原子性需要通过Synchronized这样的锁机制实现
四:volatile不支持原子性
public class Test1 {
//定义一个对象
private static int number = 0;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
number++;
}
}
});
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
}
}
上面这段代码也是很简单的一段代码,相信大家都能看明白。。按照正常情况输出结果都应该为 :10*10000=1000000。
接下来让我们执行一下看看效果,第一次执行变量number没有添加volatile关键字修饰,我们执行多次发现最终结果小于预期结果
第二次执行,这次我们使用volatile来修饰number,执行结果如下:
结果依然小于逾期结果
下面看两组代码,其实效果是一样的
代码一:
public class Test2 {
//定义一个对象
private volatile static int number = 0;
private static Object object = new Object();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
//我们使用synchronized给下面的代码加上一个同步锁
synchronized (object) {
for (int j = 0; j < 10000; j++) {
number++;
}
}
}
});
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
}
}
代码二:
public class Test2 {
//定义一个对象
private static int number = 0;
//我们使用synchronized给下面的方法加上一个同步锁
public synchronized static void add() {
for (int j = 0; j < 10000; j++) {
number++;
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
add();
}
});
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
}
}
两组代码,不管执行多少次,执行结果都是100000,与逾期结果一样
看代码我们发现我们使用到了Synchronized,它是干什么的?
Synchronized实现了线程同步锁机制,它能保证原子性。Synchronized是JVM的内置锁 ,类似的还可以使用ReentrantLock进行加锁。它们之间有什么区别,我们该怎么选择?
4.1总结
volatile不能保证原子性,要想保证原子性我们要使用锁机制。