今天来了解一下面试题:你对 volatile 了解多少。要了解 volatile 关键字,就得从 Java 内存模型开始。最后到 volatile 的原理。
大家都知道 Java 程序可以做到一次编写然后到处运行。这个功劳要归功于 Java 虚拟机。Java 虚拟机中定义了一种 Jva 内存模型(JMM),用来屏蔽掉各种硬件和操作系统之间内存访问差异,让 Java 程序可以在各个平台中访问变量达到相同的效果。
JMM 的主要目标是定义了程序中变量的访问规则,就是内存中存放和读取变量的一些底层的细节。
定义一个静态变量: static int a = 1;
线程 1 工作内存 | 指向 | 主内存 | 操作 |
---|---|---|---|
-- | -- | a = 1 | -- |
a = 1 | <-- | a = 1 | 线程 1 拷贝主内存变量副本 |
a = 3 | -- | a = 1 | 线程 1 修改工作内存变量值 |
a = 3 | --> | a = 3 | 线程 1 工作内存变量存储到主内存变量 |
上面的一系列内存操作,在 JMM 中定义了 8 种操作来完成。
主内存和工作内存之间的交互,JMM 定义了 8 种操作来完成,每个操作都是原子性的。
从上图中可知,JMM 交互在一条线程中是不会出现任何的问题。但是当有两条线程的时候,线程 1 已经修改了变量的值,但是并未刷新到主内存时,如果此时线程 2 读取变量得到的值并不是线程 1 修改过的数据。
当引入线程 2 的时候 定义一个静态变量: static int a = 1;
操作顺序 | 线程 1 工作内存 | 线程 2 工作内存 | 指向 | 主内存 | 操作 |
---|---|---|---|---|---|
-- | -- | -- | -- | a = 1 | -- |
1 | a = 1 | -- | <-- | a = 1 | 线程 1 拷贝主内存变量副本 |
2 | a = 3 | -- | -- | a = 1 | 线程 1 修改工作内存变量值 |
3 | a = 3 | -- | --> | a = 1 | 线程 1 工作内存变量存储到主内存变量,主内存变量还未更新 |
4.1 | a = 3 | a = 1 | <-- | a = 3 | 线程 2 拷贝主内存变量副本随后主内存变量更新线程 1 工作内存变量 |
4.2 | a = 3 | a = 1 | <-- | a = 3 | 线程 1 工作内存变量存储到主内存变量随后线程 2 获取主内存变量副本 |
下面就可以用 volatile 关键字解决问题。
volatile 可以保证变量对所有线程可见,一条线程修改的值,其他线程对新值可以立即得知。还可以禁止指令的重排序。
修改内存变量后立刻同步到主内存中,其他的线程立刻得知得益于 Java 的先行发生原则
先行发生原则中的 volatile 原则:一个 volatile 变量的写操作先行于后面发生的这个变量的读操作
定义一个静态变量: static int a = 1;
线程 1 工作内存 | 线程 2 工作内存 | 指向 | 主内存 | 操作 |
---|---|---|---|---|
-- | -- | -- | a = 1 | -- |
a = 1 | -- | <-- | a = 1 | 线程 1 拷贝主内存变量副本 |
a = 3 | -- | -- | a = 1 | 线程 1 修改工作内存变量值 |
a = 3 | -- | --> | a = 1 | 线程 1 工作内存变量存储到主内存变量 |
a = 3 | a = 3 | <-- | a = 3 | volatile 原则: 主内存变量保存线程A工作内存变量操作在线程 2 工作内存读取主内存变量操作之前 |
对 volatile 修饰的变量,在执行写操作的时候会多出一条 lock 前缀的指令。JVM 将 lock 前缀指令发送给 CPU ,CPU 处理写操作后将最后的值立刻写回主内存,因为有 MESI 缓存一致性协议保证了各个 CPU 的缓存是一致的,所以各个 CPU 缓存都会对总线进行嗅探,本地缓存中的数据是否被别的线程修改了。
如果别的线程修改了共享变量的数据,那么 CPU 就会将本地缓存的变量数据过期掉,然后这个 CPU 上执行的线程在读取共享变量的时候,就会从主内存重新加载最新的数据。
volatile 并不保证变量具有原子性。
public class VolatileTest implements Runnable {
public static volatile int num;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
num++;
}
}
public static void main(String[] args) {
for(int i = 0; i < 100; i++) {
VolatileTest t = new VolatileTest();
Thread t0 = new Thread(t);
t0.start();
}
System.out.println(num);
}
}
这段代码的结果有可能不是 100000,有可能小于 100000。因为 num++ 并不是原子性的。
volatile 是通过禁止指令重排序来保证有序性。为了优化程序的执行效率 JVM 在编译 Java 代码的时候或者 CPU 在执行 JVM 字节码的时候,不影响最终结果的前提下会对指令进行重新排序。
编译器会根据以下策略将内存屏障插入到指令中,禁止重排序:
全部0条评论
快来发表一下你的评论吧 !