1.volatile关键字的作用详解
概要:我们经常在java变量当中发现有些属性用volatile修饰,volatile修饰的属性到底和没用这个关键字修饰的属性有什么不同呢?
1.1 Java内存模型:定义了程序中各种变量的访问规则
首先我们要了解volatile变量的作用,就必须了解Java的内存模型,我们都知道CPU的计算速度是远远大于磁盘io,而应用的瓶颈通常也是消耗在磁盘io,网络io,访问资源等。为了使存储设备更解决cup的处理速度,我们通常会使用高速缓存来作为内存和处理器之间的缓存,提高访问效率。了解过操作系统的也知道操作系统是有很多级的缓存的什么L0,L1这些。其中最快的就是位于cpu附近的寄存器了。
所以Java为了提高性能,也有自身的一个内存缓存模型.如图
工作内存:java对于cpu各级缓存的抽象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zGeduRYD-1618142589157)(C:\Users\penghuazhang\Desktop\valatile.jpg)]
可以看出每个线程有自己的工作内存,线程对与变量的访问都是通过操作自身的工作内存来进行的。后续在通过缓存一致性协议来读取或者写入到主内存当中
2.1.保证内存的可见性:
什么是内存的可见性呢,即在并发情况下,各个线程操作同一个变量看到的都是变量的最新值.我们先来分析一下普通变量的访问流程
比如java线程1修改了一个a变量的值即从a=true改成a=false.这个过程包含线程1修改自身工作内存中a=1的值并写会到主内存中
此时java线程2也访问a变量的值, 正常情况下线程a读取到的因为CPU执行时刷新工作内存的缓存还是挺快的。不过在极端并发情况下
线程2很有可能读取到a=true也就是已经失效的值。线程2只有在线程1写会到主内存, 线程2的缓存才会失效。即线程1修改a的值对于线程2来说不说立即可见的
如何保证:
Java底层定义了一些对于volatile变量属性的访问和修改规则:
对于volatile变量的读取操作都是直接通过主内存来读取的
对于volatile变量的更新操作都是直接操作主内存的
2.2 禁止指令重排序
指令重排序是指:对于一些没用依赖的代码进行编译后的指令重排序,比如int a=1 int b=1;经过编译器的重排后很可能
先执行b=1;
为什么要禁止指令重排序呢?我们来看个例子:
public class Test {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
a = 1;
x = b;
});
Thread other = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
b = 1;
y = a;
});
one.start();other.start();
latch.countDown();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
你觉得会输出会有哪些情况呢?
输出01或者10都是正常情况,无非两个线程谁先运行,但是输出00就有问题了,因为线程 1 中的代码,编译器是可以将 a = 1 和 x = b 换一下顺序的,因为它们之间没有数据依赖关系,同理,线程 2 也一样,那就不难得到 x == y == 0 这种结果了。
当用volatile 关键之修饰变量后会在赋值操作过程中形成一个内存屏障,指令重排时不能把后面的指令重排序到内存屏障之前,当然只有一个线程访问内存中的变量的时候是不需要内存屏障的;但是如果两个线程或者更多的线程通过处理器访问同一块内存,且其他的线程在观察另一个,则时候就需要通过内存屏障来保证一致性了。
底层其实就是用volatile 关键字修饰的变量在编译后会多执行一条 “lock addl $0x0,(%esp)”这条指令有两个作用,一个是在它后面的指令不会被重排到它前面。其次它会将本处理器的缓存写到主内存中,并且改写入动作会导致其他cpu中的缓存失效。通过这样一个操作可以让前面volatile 变量的修改对其他处理器立即可见