volatile关键字的作用
要想知道volatile关键字的作用,我们可以从下面的概念逐层了解。
内存模型的相关概念
Java内存模型(JMM)
Java虚拟机规范试图定义了一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。简单的来说,这是由于CPU执行指令的速度远大于内存访问的速度,因此就在CPU里面添加几层缓存用于提高访问速度。
存在问题
为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者告诉缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。简单的来说,在Java内存模型中,就会存在缓存一致性和指令重排序的问题。
举例说明
在Java中,执行下面语句
i = 7;
执行线程必须现在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中,而不是直接将数值7写入主存中。
原子性、可见性、有序性介绍
那么Java语言本身对原子性、可见性、有序性提供了哪些保证呢?
原子性
在Java中,对基本数据的变量读取和赋值操作是原子性操作,即这些操作是不可中断的,要么执行,要么不执行。看下面代码:
x = 7; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值7赋值给x,也就是说线程执行这个语句会直接将数值7写入到工作内存中。 语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存这两个操作都是原子性操作,但是合起来就不是原子性操作了,因为在存取过程中,其他线程可以引起变量的变化。 语句3,4和2一样,也就是说,只有简单的读取、赋值 从上面可以看出,Java内存模型值保证了基本的读取和赋值是原子操作,如果要实现更大操作的原子性,可以通过synchronized和Lock来实现。
#####可见性 对于可见性,Java提供了volatile关键字来保证可见性。 可见性是指多个线程访问同一个变量时,其中一个线程修改了该变量的值,其他线程能够立即看到修改的值 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新的主存,当有其他线程需要读取时,它会去内存中读取新值。
其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
代码
下面通过代码解释可见性的含义:
public class Main {
private static boolean flag = true;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("线程1开始执行了");
while (flag) {
}
System.out.println("线程1执行结束了");
});
Thread thread2 = new Thread(() -> {
System.out.println("线程2开始执行了");
while (!flag) {
}
System.out.println("线程2执行结束了");
});
thread1.start();
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
thread2.start();
}
}
上面代码开启两个线程,当flag为true时,会一直陷入死循环中,当修改flag = false时,线程1仍然陷入死循环中,说明线程1并不能立即看到修改的值。运行结果如下:
线程1开始执行了
线程2开始执行了
下面为flag添加volatile修饰时,即
private volatile static boolean flag = true;
运行结果下如:
线程1开始执行了
线程1执行结束了
线程2开始执行了
当主线程修改flag的值后,线程1能立即看到修改后的值。
有序性
JMM允许编译器和处理器对指令重排序的,使用volatile关键词,可以禁止重排序,可以确保程序的“有序性”。
总结
volatile 修饰的变量的有序性有两层含义:
- 所有在 volatile 修饰的变量写操作之前的写操作,将会对随后该 volatile 修饰的变量读操作之后的语句可见。
- 禁止 JVM 重排序:volatile 修饰的变量的读写指令不能和其前后的任何指令重排序,其前后的指令可能会被重排序