1. 基本概念
1.1 volatile
volatile与synchronized一样是一个Java的关键字。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。
1.2 volatile的特性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止指令重排序。(实现有序性)
- volatile只能保证对单次读/写的原子性。
1.3 指令重排序
为了优化程序性能,编译器和处理器会对java编译后的字节码和机器指令进行重排序,通俗的说代码的执行顺序和我们在程序中定义的顺序会有些不同,只要不改变单线程环境下的执行结果就行。但是在多线程环境下,这么做却可能出现并发问题。
1.4 并发的三大特性
- 原子性
- 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。
- 可见性
- 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 有序性
- 因为“线程内表现为串行的语义”,所以如果在本线程内观察,所有的操作都是有序的;
- 因为“指令重排序”现象和“工作内存与主内存同步延迟”现象,所以如果在一个线程中观察另一个线程,所有的操作都是无序的。
2. volatile使用方法
与synchronized不同,volatile只能修饰变量,所以这里通过volatile的三个特性来介绍使用方法
2.1 volatile实现可见性
可见性是指当多个线程操作同一变量时,一个线程对该变量的修改,其他线程可以感知到。
2.1.1 场景模拟
现在模拟一个施工单位和监理单位的关系,即施工单位干活,监理单位确认施工进度。
public class App01 {
// 模拟施工和监工
// 假设有一个人在盖房子,然后边上有一个人帮他数着,必须是盖一层,数一次,再接着盖一层
// 当前楼层
private static int count = 0;
// 盖100层楼
private final static int sum = 100;
public static void main(String[] args) {
// 施工
new Thread(() -> {
while (count < sum) {
System.out.println("盖了第" + (++count) + "层");
// 休息2秒钟让监工看一下
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "worker").start();
// 监工
new Thread(() -> {
// 先将当前的楼层记下来
int localValue = count;
while (count < sum) {
// int localValue = count;
// 如果楼层发生变化
if(localValue != count) {
System.out.println("确认盖了第" + count + "层");
localValue = count;
}
}
}, "looker").start();
}
}

在不加volatile的情况下,监工似乎感知不到楼层的变化
这是因为监理线程把数据读到自己的工作空间后,他也不写入,那么他就一直用这个值就行了,他也不知道别人改没改。(注意:这里有个错觉,就是以为监工在记录楼层是发生在while循环外面,所以没有实时读到,但其实即使把localValue的赋值拿到while里面来,一样是看不到监工的活动的)。
这里我们修改一下变量的定义,增加volatile关键字来解决这个问题
private volatile static int count = 0;
这时我们的结果会变成下面这种情况

其实已经基本完成需求了,但还有一个小问题是没有数第一层。
那为什么没有数第一层呢,这个是我代码逻辑的问题,监工的责任是每比之前多盖一层我就数一层,但是在一开始的时候,施工单位现动工了,那第一层监工就不知道以哪一层为参照物来确定这第一层是不是新盖的,所以他只能认为这不是新盖的,解决办法是两个线程互换位置,即先运行监理线程,后运行施工线程
2.2 禁止指令重排序
这个没有找到一个很好的例子来介绍,因为会涉及到编译后的字节码,我大致讲解一下原理
假设我们现在有这么一段代码
public class App02 {
public static void main(String[] args) {
boolean a = false;
int b = 15;
a = true;
}
}
.java文件,经过编译后生成字节码文件,在编译过程中,由于系统优化等等原因,他可能对代码的执行顺序进行修改,但不影响你整体程序在单线程情况下的运行结果。
比如上面这个例子中a赋值,b赋值,然后又是a赋值,而a和b是不关联的操作,那么编译时就有可能被优化成下面这种执行顺序
boolean a = false
a = true;
int b = 15;
也就是他也许认为我先把a的事情做完了,再去做b的事情,这并不影响最后的运行结果,但如果代码这样编写的话
public class App02 {
public static void main(String[] args) {
boolean a = false;
volatile int b = 15;
a = true;
}
}
那么b之前的代码不能优化到b后面执行,b后面的代码也不能优化到b前面执行
2.3 volatile只能保证读、写的原子性
什么是原子性,就是不可分割的操作,比如a=1,System.out.println(b)这种操作都是原子性的,我给a赋值1,赋值了就完事了,没有多余的操作,读取也是,我读出来输出就完事了,同样没有多余的操作。
之前施工和监理的程序就是一个变量不断增加,但由于只有一个线程在修改,所以看不出原子性的影响,我们对程序进行一些修改
现在假设我们有个大的施工公司,他可以派出10个施工队来盖这100层的房子,大家轮流盖,这样快一点,然后我们这次不用监理了,代码如下
public class App03 {
// 假设有一个施工公司在盖房子,他将派出10个施工队来盖100层
// 当前楼层
private volatile static int count = 0;
// 盖100层楼
private final static int sum = 100;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 施工队
new Thread(() -> {
while (count < sum) {
System.out.println(Thread.currentThread().getName() + "盖了第" + (++count) + "层");
// 盖累了休息2秒钟
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "worker"+i).start();
}
}
}
我们预期的结果是,一共十个worker,worker0改了第一层,然后worker1盖了第二层,worker2盖了第三层。。。这样十个施工队轮流施工,最后盖好了100层,但结果是这样的

这里我就只放最后这一段(每个人因为系统、CPU不同可能结果不同),可以看到,不仅没有按顺序盖,还多盖了一层。这就是因为++count操作不满足原子性,他其实是分为三步的,他既要从主内存中读取,又要修改,最后还要刷新到主内存中。而我们的volatile并不能保证这些非原子操作的原子性,所以最后出现了乱序和错误。