Volatile的定义 |
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
上面的定义有两层含义:
1,保证共享变量的可见性,但无法保证原子性
2,防止指令重排
Volatile的可见性 |
一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
可见性对于共享变量在多CPU并行执行程序尽量保证不出错的一个很重要的特性。
因为线程所执行需要的数据都在主存中,而对数据的操作是由CPU来完成的,每次CPU需要数据都去主存中取,这样是很不科学的,CPU执行的速度很快,而主存完全跟不上节奏。所以每个CPU都有自己私有的高速缓存区,在执行操作之前,将需要的数据从内存中复制到CPU高速缓存区,计算完毕后,将结果重新写回主存。
单线程操作时,不会发生问题,但是在多线程的情况下就不一样了。
我们来一个小demo |
public class IncrVolatileVal {
public static int i =0;
static class MyThread{
public static void start() {
Thread t = new Thread(()->{
for(int j = 0 ; j<1000;j++){
i++;
}
});
t.start();
}
}
public static void main(String[] args) {
for(int i =0 ;i<5;i++){
IncrVolatileVal.MyThread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while("main".equals(Thread.currentThread().getName())) {
System.out.println(IncrVolatileVal.i);
break;
}
}
}
这是一个每个线程为 i 变量加1000次,5个线程同时工作的小程序,但是我们取的最后结果总是小于5000。
我们可以大致模拟小于的情况:
A线程先从主存取出 i = 8并且复制到自己的高速缓冲区的时候,有个B线程也从主存读取到了8,但是它成功将9写入主存,但是A线程还是以为8是最新值,做着加1操作,所以A线程的这次任务提交失败。所以导致加完之后的总数小于5000。
这是因为 i 的值被更新了,但是其它线程操作的值还是自己高速缓冲区的值 。
volatile定义是保证共享变量在内存的可见性,就是共享变量被其它线程更新了值,其它的高速缓冲区能立马知道变化,并且更新自己缓冲区的值。那是不是我们将 i 用volatile修饰就能得到结果呢?
我们来一个测试volatile修饰共享变量的小demo |
public class IncrVolatileVal {
public static volatile int i =0;
static class MyThread{
public static void start() {
Thread t = new Thread(()->{
for(int j = 0 ; j<1000;j++){
i++;
}
});
t.start();
}
}
public static void main(String[] args) {
for(int i =0 ;i<5;i++){
IncrVolatileVal.MyThread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while("main".equals(Thread.currentThread().getName())) {
System.out.println(IncrVolatileVal.i);
break;
}
}
}
但是结果还是小于5000是怎么回事呢?
还是分析一下:
还是A线程取到 i 的值为8,但是这是B线程修改了 i 现在为9了。由于 i 是volatile 修饰,A线程将自己的高速缓存更新为 i = 9。然后开始对 i 进行运算,但是还是会出现提交失败,A 取到的9是当时的最新值,但是 i++的过程是,先取 i 的值,在将 i 的值加1,在将 i+1的结果赋值给 i 。然后问题就出现在这里,这是3个原子操作,但是合在一起就并不是原子操作了。在某一步,可能其它线程把值给改了,然后导致这次任务提交失败。
这次失败是因为 i++操作 并不是原子操作。
这两个例子对比,可以体现出了volatile保证共享变量的可见性,但无法保证原子性
Volatile的防止指令重排 |
在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:
1,编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2,处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
看的书都有介绍下面语句:
volatile防止指令重排是通过内存屏障实现的,被volatile修饰的变量,在汇编语句中会有个lock前缀。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
然后我准备了单例模式的双重校验锁实现做demo,那个volatile起到防止重排的作用。
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
通过jvm参数来看汇编指令,然后 给我报了个
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled
差了一个插件,过几天在配。现在还要抓紧继续下去。