volatile
volatile是什么
volatile 是一个类型修饰符,使用方式如下
private volatile int a = 0;
线程安全的前提
-
原子性
一个或者多个操作,要么全部执行并且中途不能被打断,要么都不执行。 -
可见性
同一个线程里,先执行的代码结果对后执行的代码可见,不同线程里任意线程对某个变量修改后,其它线程能够及时知道修改后的结果。 -
有序性
同一线程里,程序的执行顺序按照代码的先后顺序执行。
只有满足了以上三个前提,才能说线程是安全的
volatile的作用
volatile关键字在多线程中,只保证可见性、有序性。但不保证原子性。
1. 保证可见性
来看一个网上找的例子
public class TestVolatele {
//测试一
private static boolean isOk = true;
//测试二
//private static volatile boolean isOk = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "--开始循环了");
while (isOk) {
}
System.out.println(Thread.currentThread().getName() + "--跳出循环了");
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (Exception e) {
}
isOk = false;
System.out.println(Thread.currentThread().getName() + "--isOk改为 false");
}, "t2").start();
}
}
这个例子很简单,t1线程会一直监听isOk字段,t2线程负责修改isOk字段,正常情况下,当t2把isOk改为false时,t1应该会退出while循环,运行代码来验证一下结果。

上图是没有加volatile关键字的运行结果。可以看到线程 t1 并没有执行完。
这说明线程 t2 修改了 isOk = false 之后,在线程 t1 中并不知道该字段被修改了。

上图加volatile关键字的运行结果。线程t1也执行完了。
可以看出加了volatile关键字,那么isOk就具备了可见性。

为了解释可见性的原因,可以看上图的java内存模型图。isOk = true这个字段其实时存在主内存中的。当线程要操作isOk时,先把isOk复制一份到自己的工作内存中,在工作内存中对字段操作完后,会再把字段写入主内存。
假设没有volatile字段时
1、t1 线程从主内存复制 isOk = true 到 t1 工作内存
2、t2 线程从主内存复制 isOk = true 到 t2 工作内存
3、t2 修改工作内存 isOk = false,并赋值给主内存(主内存isOk = false)
4、t1 线程读取的还是 t1 工作内存(isOk= true),并不知道主内存isOk已改为false
5、由于无法实时获取主内存最新数据,所以导致一直while循环
有volatile字段时
1、t1 线程从主内存复制 isOk = true 到 t1 工作内存
2、t2 线程从主内存复制 isOk = true 到 t2 工作内存
3、t2 修改工作内存 isOk = false,并赋值给主内存(主内存isOk = false)
4、isOk 由于加了volatile关键字,这时 t1 线程强制读取主内存数据
5、读取到主内存isOk=false,退出while循环(可以理解为volatile关键字对主内存保证可见性)
2.保证有序性(禁止指令重排序)
什么是有序性?我们写的Java程序代码不总是按顺序执行的,都有可能出现程序重排序(指令重排)的情况,这么做的好处就是为了让执行块的程序代码先执行,执行慢的程序放到后面去,提高整体运行效率。

int a = 1;
int b = 2;
上述的两条赋值语句在程序运行时,并不一定按照顺序先给a赋值,然后再给b赋值,很有可能先执行b再执行a,这是程序为了提高效率出现了指令重排序。虽然在单个线程中,指令重排序不会对结果产生任何问题,但是在多线程中出现指令重排序,可能会导致最终的结果不是我们想要的。
举例个单例模式(懒汉式)的例子
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
如果上边的代码不使用volatile关键字,可能会出现问题。问题在于 instance = new Singleton();这行代码,其实这行代码可以拆分为
1、为instance分配内存
2、初始化instance
3、将instance变量指向分配的内存空间
现有A和B两条线程同时调用 getInstance() 方法,假设A线程先执行了instance = new Singleton() 并且发生了指令重排序。可能会出现A线程先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,B线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。所以在单例的懒汉式中需要加上volatile关键字禁止指令重排序
3.不保证原子性
public class TestVolatele {
private static volatile long n = 0;
public static void main(String[] args) throws Exception {
List<Thread> tList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
tList.add(new Thread(() -> {
for (int j = 0; j < 2000; j++) {
n++;
}
}));
}
for (Thread thread : tList) {
thread.start();
}
for (Thread thread : tList) {
thread.join();
}
System.out.println(n);
}
}
上面的代码开启5条线程,每条线程对n++两千次。如果volatile关键字具备原子性,那么结果肯定等于10000。但实际上每次运行的结果都不同,结果中n总是 <= 10000,这说明volatile不具备原子性。
但可能会疑惑,volatile关键字不是直读取主内存吗?明明可以实时拿到到主内存的最新数据,为什么还不保证原子性?这就需要把n++给拆分来解释
可以把n++拆分为3个阶段
1、读取 n
2、对 n 加 1
3、把 n 写入主内存
把n++拆分之后,再来分析结果 n <= 10000 的原因
假设A、B两条线程操作 n++,并且n初始值为0
1、A 加载主内存 n=0 到 A 工作内存
2、B 加载主内存 n=0 到 B 工作内存
3、A 在工作内存中执行 n++ ,把结果 n=1 写入主内存
4、B 强制读取主内存 n = 1 ,并执行 n++ 操作(到这里都没问题)
5、但是,B 在执行 n++ 时,只执行了 n++ 的第1步,读取 n=1(这时 B 就停止了,CPU切换到A线程执行)
6、此时线程 A 执行了 n++ ,并且执行完了,把结果写入主内存 n=2
7、CPU又切换到 B 执行了,B 执行 n++ 的第2步,对n加1。(此时 B 中 n=2)
8、B 执行完后,把 n=2 写入主内存,这就导致了主内存中写入了两次 n=2
上边的举例由于主内存写入两次 n=2,所以最终导致 n <= 10000 的,所以说 volatile 不保证原子性。 volatile 它只针对读取时可见,既读取时的数据保证最新的,但是并不保证写入数据时不存在问题。
volatile应用场景
volatile的应用要从它的特性入手,只保证可见性、有序性。但不保证原子性。
(1)volatile最适合使用的地方是一个线程写、其它线程读的场合,如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。
(2)假如一个线程写、一个线程读,根据前面针对volatile的应用总结,此时可以使用volatile来代替传统的synchronized关键字提升并发访问的性能。
(3)volatile不适合多个线程同时写的情况,因为volatile不保证原子性,多线程同时写会有问题

浙公网安备 33010602011771号