volatile 关键字的两层语义
1)可见性 **ps:volatile不能保证原子性**
简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:
- 线程对变量进行修改之后,要立刻回写到主内存。
- 线程对变量读取的时候,要从主内存中读,而不是缓存。
2)禁止进行指令重排序 [**这个有人测出并不能完全保证禁止指令重排序**]
什么是指令重排序?有两个层面:
- 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。
- 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。
/**
* 说明 :
* 1> 死循环中只要做些逻辑处理,即使flag不进行volatile修饰,就可以停止死循环
* 2> 死循环中如果不做逻辑处理,如flag不进行volatile修饰,则不会终止死循环
* 3> 如果flag进行volatile修饰,则会终止死循环
* 4> 结论 : 仅为我猜测 如若有不同理解的可以留言
* 1> 即使flag不进行volatile修饰,如若空闲,虚拟机也会回写主内存,如若对变量读取频率很高,不进行回写主内存
* 2> 如果flag进行volatile修饰,则会强制进行回写主内存
*/
public class ThreadTest {
private static volatile boolean flag = true; // 定义变量
public static void main(String[] args) {
// 启动线程t1,死循环执行
new Thread(new Runnable() {
@Override
public void run() {
while(flag) {
try {
Thread.sleep(1); // 增加这行,即使flag不使用volatile,也会取到修改后的值,停止运行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.err.println("flag is false , stop . ");
}
}).start();
// 启动线程t2,修改变量值,终止死循环
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag为 false . ");
}
}).start();
}
}
补充一下volatile的适用场景
并发专家建议我们远离volatile是有道理的,这里再总结一下:
- volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。
- 如今非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
- volatile不能保证原子性,这点是大家没太搞清楚的,所以很容易出错。
- volatile可以禁止重排序。
所以如果我们确定能正确使用volatile,那么在禁止重排序时是一个较好的使用场景,否则我们不需要再使用它。用专业点更广泛的说法就是“对变量的写操作不依赖于当前值且该变量没有包含在其他具体变量的不变式中”