JVM虚拟机规范中曾经试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都可以达到一致性的内存访问效果。
然而定义这样一套内存模型并非很容易,这个模型必须足够严谨,才能让Java的并发内存访问操作不会有歧义。但是也必须足够宽松,这样使得虚拟机的具体实现能够有自由的发挥空间来利用各种硬件的优势。经过长时间的验证和弥补,到了JDK1.5(实现了JSR133规范)之后,Java内存模型才终于成熟起来了。
Java内存模型规定了所有的变量都存储在主内存
(Main Memory)中,每条线程都有自己的工作内存
(Work Memory)
原子的
,不可再分
的)lock 锁定
: 作用于主内存,将一个变量标识为线程独占状态unlock: 解锁
: 作用于主内存,将一个线程独占状态的变量释放read 读取
: 从主内存读取数据到工作内存,便于之后的load操作load 载入
: 把read读取操作从主内存中得到的变量放入工作内存的变量副本中use 使用
: 将工作内存中的变量传递给执行引擎 当虚拟机遇到一个需要使用变量值的字节码时,执行此操作assign赋值
: 将执行引擎中的值赋给工作内存的变量。 当虚拟机遇到一个赋值操作时,执行此操作store存储
: 将工作内存的值传递到主内存 ,便于之后的write操作write写入
:将store存储操作中从工作内存中获取的变量写入到主内存中 volatile可以说是Java虚拟机提供的最轻量级的同步机制。但是它并不容易被正确,完整的理解。
Java内存模型中规定
当一个变量被定义为volatile
之后,表示着线程工作内存无效,对此值的读写操作都会直接作用在主内存上,
因此它具备对所有线程的立即可见性
。
立即可见性
当变量的值被修改之后,新值对于其他线程是立即可知的。普通变量并不能做到这一点,因为普通变量的值在线程之间的传递是要进过主内存来完成的。比如当线程A对变量进行了回写操作,线程B只有在A回写完成之后,在对主内存操作,新值才对B是可见的。在A回写到主内存的过程中,B读取的依旧是旧值。
但是这并不可以推导出基于volatile变量的运算在并发下是安全的
,因为在Java中的运算操作符并不是原子性
的。这导致了volatile变量在并发下运算是不安全
的。
通过代码验证volatile变量在并发下运算是不安全
首先我们创建20个线程,每个线程对volatile变量进行1000次的自增操作。
/**
* @作者: 写Bug的小杜 【email@shaoxiongdu.cn】
* @时间: 2021/07/31
* @描述: 通过代码验证 【volatile变量在并发下运算是不安全】
*/
public class VolatileTest {
//volatile修饰的count
private static volatile int count = 0;
//count自增方法
public static void increment(){
count++;
}
public static void main(String[] args) {
//对count进行递增1000次操作的可运行接口
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始对count进行递增操作");
for (int i = 0; i < 1000; i++) {
increment();
}
System.out.println(Thread.currentThread().getName() + "线程对count递增操作结束");
}
};
// 创建20个线程并启动
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(runnable);
thread.setName((i+1) + "号线程");
thread.start();
}
while (Thread.activeCount() > 2){
//主线程回到就绪状态
Thread.yield();
}
System.out.println("所有线程结束,count = " + count);
}
}
如果此程序在并发下是安全的,那么count的值最后肯定是20*1000 = 20000;也就是说,如果运行结果为20000,那么volatile变量在并发下运算是安全的
通过多次运行程序,我们发现,count的值永远比20000小。
那么,这是为什么呢?
我们将上方的代码进行反编译,然后分析increment方法的字节码指令。
0 getstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I>
3 iconst_1
4 iadd
5 putstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I>
8 return
我们可以发现,一行count++代码被分为 4行字节码文件去执行。通过对字节码的分析,我们发现,
当偏移量为0的字节码getStatic将count的值从局部变量表取到操作数栈顶的时候,volatile
保证了此时count的值是正确的,但是在执行iconst_1, iadd这些操作的时候,其他线程已经把count的值改变了,此时,操作数栈顶的count为过期的数据,所以putStatic字节码指令就有可能将较小的值同步到主内存中。因此最终的值会比20000稍微小。
也就是说,volatile变量在并发下运算是不安全的
。
在并发环境下,volatile的变量只是对全部线程即时可见的,如果要进行写的操作,还是要通过加锁来解决。