被volatile 修饰的变量具有可见性与有序性。这也是我们使用volatile关键字的原因。我们先了解一下什么是可见性和有序性,就能明白什么时候可以使用volatile 关键字了。
可见性
-
在java的内存模型中,基于效率的原因,每个线程会从主内存中拷贝一份变量的副本到的工作内存中使用。这其中就有一些问题
- 如果线程1拷贝了一个变量A到自己的工作内存中
- 在线程1还未操作变量A的副本之前,线程2就修改了主内存中变量A的值,
- 而由于线程1的工作内存中已经有了变量A的副本,它会直接使用变量A的旧值进行操作,使得变量A的新值对线程1并不是立刻可见的
-
所以可见性的含义就是指:如果一个线程更新了一个共享的变量,在这一刻起其它线程读取使用的应该就是新的变量值,新的变量值对于其它线程来说应该是立刻可见的。
有序性
一样是在java的内存模型中,允许编译器和处理器对指令进行重排序。虽然重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。所以在多线程的环境之中为了并发执行的正确性,我们就会要求部分代码能按我们代码本身的顺序执行,这就是有序性。
那volatile 关键字是如何做到可见性与有序性的呢?
- 使用volatile关键字的变量被修改后会强制将修改的值立即写入主存,并让其它工作内存中缓存(副本)无效;
- volatile关键字禁止指令重排,但只是禁止对volatile关键字修饰的变量的读写指令进行重排。volatile关键字能保证前面的所有指令都执行完了,才开始执行这条指令,在这条读写指令执行之后,后续的指令才能开始执行。
volatile的原理和实现机制
在volatile关键字修饰的变量的写指令之后(读之前),虚拟机会添加一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2. 它会强制将对缓存的修改操作立即写入主存;
3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile的使用
volatile关键字只具有可见性和有序性,而没有原子性,也不会加锁导致线程阻塞,这使得它是比synchronized关键更轻量级的同步机制。但是也正因为volatile不具有原子性,它的同步要求也更苛刻。
什么是原子性?
最基本的解释是:指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。
- 如果从程序的层面上来看,一个操作具有原子性,它要么全部完成,要么全部不完成,不可能停滞在中间某个环节。
- 从数据库的角度来说,具有原子性的数据操作集合,要么数据全部都更新好了,要么出错了必须回滚让数据全部不被更新,不允许更新部分的情况。
- 而在多线程的层面上,一个操作或操作集,如果一旦操作开始,操作的执行不会受到其他线程的影响,那这也是在多线程层面上的表现出的原子性。
所以原子性是个很抽象的概念,在不同的方面上它的表现形式是不一致的。按具体需求具体分析。
而这里我们主要讨论的是在多线程的层面上,在多线程的层面的原子性就是指一个操作或操作集不能同时被两个或两个以上的线程执行。
synchronized关键字,通过锁让一段程序一次只能让一个线程进来访问,实现了多线程层面上的原子性。但很明显volatile关键字是修饰的是变量,它与操作是无关,它不具有实现原子性的手段。
没有原子性会怎么样?
同步的三要素是:可见性,有序性,原子性。没有了原子性,volatile做不到绝对的同步。我们可以举一个例子
//定义
volatile int i;
//可多线程访问的方法
void inc(){
i++;
}
-
在编译之后,i++这段代码会编译为三个指令,
- 获取i的值,
- 值加上1,
- 将新值赋给变量i
-
这就出现了一个问题,如果一个线程还未执行好第三个指令的时候,另一个线程已经执行完了第一个指令并获取i值(旧的),那么就会发生i++语句虽然执行了两次实际只自增了一次的结果。这显然与我们需要的结论不一致,我们需要i++这段代码具有原子性保证结果正确。
所以对于需要原子性的同步操作,我们不能用volatile去约束,最普遍的就是如果变量的新值是由旧值所决定的,那就不能用volatile去实现同步,要用锁或者atomic原子类。
volatile 关键字适用哪里?
使用volatile 变量必须满足以下三个条件
- 当变量的写操作不依赖变量的当前值,或者确保只有单个线程能更新该变量
- 该变量不会与其它变量一起纳入不变性条件(不变性条件不是很理解)
- 在访问时不需要加锁
比如最常见的用于修饰共享的状态。例子如下:
- 主线程通过tate控制demo线程的结束。state哪怕被其它的几个线程同时修改为true也都不会影响程序的正确性。
public class VolatileDemo extends Thread {
volatile boolean tate = false;
@Override
public void run() {
while (!tate) {
// 。。。
Thread.yield();
}
System.out.println("run end");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
demo.start();
Thread.sleep(1000);
demo.tate = true;
}
}