一、简介
volatile,是Java中的一个关键字。被volatile修饰的变量,在多个线程中保持可见性,注意,volatile不保证原子性,这也是volatile与synchronized的区别之一。
那么什么是可见性呢?
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
要了解volatile的工作原理,首先需要了解一下Java内存模型,下面是粗略图:
在Java中,每个线程都有一个独立的内存空间,称为工作内存。 它保存了用于执行操作的不同变量的值。在执行操作之后,线程将变量的更新值复制到主内存中,这样其他线程可以从那里读取最新值。
二、使用场景 - 保证内存可见性
如果需要保证某个变量在多个线程之间可见性的时候,可以使用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。
三、使用场景 - 禁止指令重排序
指令重排序,简单理解就是说,保证代码按照我们写的顺序执行。被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(),实际上可分为三个步骤:
//分配对象的内存空间
//初始化对象
//设置instance指向刚分配的内存地址
操作2依赖1,但是操作3不依赖2,所以有可能出现1,3,2的顺序,当出现这种顺序的时候,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。
四、使用场景 - 不保证原子性
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的,覆盖成了4,这就存在了原子性问题。
五、总结
volatile关键字保证内存可见性,禁止指令重排序,但是注意它不保证原子性,所以volatile不能完全替代synchronized关键字,因为synchronized保证原子性的。