目录
一、简介
volatile,是Java中的一个关键字。被volatile修饰的变量,在多个线程中保持可见性,注意,volatile不保证原子性,这也是volatile与synchronized的区别之一。
volatile关键字只保证可见性,不能保证原子性。
- 什么是可见性?
可见性是指当多个线程访问同一个变量时,一个线程如果修改了这个变量的值,其他线程能够立即看得到修改之后的值。
在了解volatile的工作原理之前,首先需要了解一下Java内存模型,如下图:
在Java中,每个线程都有一个独立的内存空间,称为工作内存。 它保存了用于执行操作的不同变量的值。在执行操作之后,线程将变量的更新值复制到主内存中,这样其他线程可以从主内存中读取最新值。
二、volatile使用场景 - 保证可见性
如果需要保证某个变量在多个线程之间可见性的时候,可以使用volatile关键字进行修饰。我们来看一个例子:
public class T03_Volatile {
/**
* 如果没有加volatile修饰,线程A由于死循环,可能没有及时从主内存读取最新的running值
* 加了volatile修饰,一旦running的值发生变化,就会通知其他线程需要从主内存重新获取值
*/
private volatile boolean running = true;
private void m1() {
while (running) {
System.out.println("hello....");
}
}
public static void main(String[] args) {
T03_Volatile t03_volatile = new T03_Volatile();
new Thread(t03_volatile::m1, "A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t03_volatile.running = false;
}
}
注意:上述程序有可能会出现在没加volatile关键字的情况下,线程A也能及时读取到最新的running值,这主要是由于CPU可能暂时空闲,自动从主内存同步了最新的running到线程A中,导致线程A执行结束。
三、volatile使用场景 - 禁止指令重排序
指令重排序,简单理解就是说,保证代码按照我们写的顺序执行。被volatile修饰了的变量,禁止了指令进行重排序,所以可以保证代码完全按照我们编写的顺序执行(不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的)。
比较典型的例子就是双重检查机制的单例模式,简称DCL单例,具体代码如下:
高频面试题:DCL(Double Check Lock)双重检查单例模式,为什么需要加volatile关键字?主要考点其实就是volatile禁止指令重排。
public class T03_Volatile {
/**
* 加入volatile关键字修饰
*/
private volatile static T03_Volatile instance = null;
private T03_Volatile() {
}
public static T03_Volatile getInstance() {
if (null == instance) {
synchronized (T03_Volatile.class) {
if (null == instance) {
instance = new T03_Volatile();
}
}
}
return instance;
}
}
注意我们创建实例的语句 instance = new T03_Volatile(),实际上底层分为三个步骤:
- 1、分配对象的内存空间;
- 2、初始化对象;
- 3、设置实例对象指向刚分配的内存地址;
分析:
步骤2【初始化对象】需要依赖于步骤1【分配对象的内存空间】,但是步骤3【设置实例对象指向刚分配的内存地址】不需要依赖步骤2【初始化对象】,所以有可能出现1,3,2的执行顺序,当出现这种顺序的时候,虽然instance不为空,但是此时对象有可能没有正确初始化,直接拿来使用的话可能会报错。
四、volatile使用场景 - 不保证原子性
volatile关键字不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。我们看一个案例:
public class T03_Volatile {
private static CountDownLatch countDownLatch = new CountDownLatch(500);
private volatile static int num = 0;
public static void main(String[] args) {
for (int i = 0; i < 500; i++) {
new Thread(() -> {
try {
TimeUnit.MICROSECONDS.sleep(400);
num++;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num = " + num);
}
}
运行结果:多运行几次,发现结果不固定,有时候会是500,有时候是495,有时候是497,这就证明了volatile不保证原子性。
原因解析:
num++操作并不是原子操作,实际上num++的操作分成了三个步骤进行:
//获取变量的值
int temp = num;
//将该变量的值+1
num = num + 1;
//将该变量的值写回到对应的主内存中
num = temp;
举一个例子:
假设线程A首次拿到的num = 3,在执行+1操作前,可能存在其他多个线程已经对num做了修改,假设此时主内存中的num已经被修改到20了,而此时线程A执行+1操作,将num=3+1=4的结果又重新写回到了主内存中,将原本num应该是num = 20 + 1 = 21的,被线程A回写之后覆盖成了4,因此总的结果小于或者等于500,这就存在了原子性问题。
五、总结
本篇文章介绍了volatile关键字的基本用法,以及通过案例讲解了volatile在内存可见性、指令重排序方面的作用。总结一句话:
volatile关键字保证内存可见性,能禁止指令重排序,但是注意它不保证原子性,所以volatile不能完全替代synchronized关键字,因为synchronized保证原子性的。