上一篇文章中我们介绍了 Synchronzied ,接下来我们来介绍 volatile。
我们知道 Java 内存模型有三大特性,有序性、可见性、原子性。
我们首先来谈一谈有序性。
那么什么是有序性呢?Talk is cheap,show me the code 我们直接来看代码。
public static void main(String[] args) {
int num1=10;//代码1
int num2=5;//代码2
num1=num1-num2;//代码3
num2=num1*num1;//代码4
}
这段代码的执行顺序一定是代码1、2、3、4 吗?这是不一定的,为什么呢?因为可能会发生指令重排序。指令重排序就是在不影响程序最终结果的前提下,为了提升程序的执行效率,可能会对输入的代码顺序进行优化,造成执行顺序不一定按照代码书写顺序的一种形式。
我们来看看指令重排序的定义,一个必要的条件就是不影响程序的执行结果,那么想想看我们的代码3和代码4的执行顺序能否调换顺序呢?答案自然是否定的,因为代码4需要依赖代码3的内容。
在单线程的情况下,重排序不会影响程序的运行结果,那么在多线程情况下呢?我们来看一看。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
首先在线程1中先执行语句2,然后切换到线程2来执行。这个时候线程二认为初始化已经完成,就会进行接下来的操作,但是 context 并没有初始化,所以就会导致代码出错。
接下来是可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
我们使用 volatile 关键字可以保证可见性,使得各个线程都可以读取到最新的值
最后是原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
我们来看一个最简单的例子,代码 i++ 是原子性操作吗?其实 i++ 可以分为3步来看待。
- 读取 i 值
- 对 i +1
- 把新值赋给 i
这其中任何一步发生都可能发生线程安全问题,所以 i ++ 不是原子性操作。
介绍完我们的三大特性,我们回过头再来说 volatile , volatile 关键字可以做到可见性和有序性。也就是可以保证变量获取最新的值以及禁止指令重排序,但是它不能实现原子性,所以 volatile 的使用具有局限性,我们简单地来总结一下它的使用场景。
- 对变量的写入操作不依赖当前的值,或者你能确定只有单个线程能够更新变量的值
- 该变量不会和其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁