java 并发——volatile
介绍
*: volatile 是一个类型修饰符(type specifier).volatile 的作用是确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
看了上面的话感觉不是那么的好理解,因为 volatile 关键字是和 java 内存模型 JMM(java memory model)是息息相关的,所以在介绍 volatile 之前我们先来看一下 java 内存模型.
内存模型概念
我们的程序执行指令是从 cpu 中执行的,程序执行肯定和数据脱不开干系,这些数据肯定都是在计算机的物理内存中进行.但是随着 cpu 越来越牛x,而内存的话就会显得相对来说不那么效率,这就导致每次 cpu 执行导致每次操作数据都要去内存操作,这样就很耗费时间.
由于计算机的存储设备与 cpu 的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
解决了效率的问题但是随之而来也就出现了新的问题需要解决——缓存一致性.因为在程序运行期间会将运行所需要的数据从内存中复制一份到 cpu 高速缓存中之后再进行对应的运算,再运算期间数据的读取是从高速缓存直接读取并不会再取读取内存中的数据了,只有再运算结果出来后才会将数据重新刷新会到内存中.
解决方法:
- 通过数据总线加锁的方式 LOCK.(但是这样效率极为低下不建议)
- 缓存一致性协议.(确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 cpu 在写数据时,如果发现操作的变量是共享变量,则会通知其他 cpu 告知该变量的缓存行是无效的,因此其他 cpu 在读取该变量时,发现其无效会重新从主存中加载数据。)
java 内存模型
java 内存模型规定了所有的变量都存储在主内存中,每条运行的线程还是各自的工作内存区域,线程运行的是会从主内存中将变量从主内存拷贝一份到自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不是直接操作主内存,不同的线程之间工作内线是不会共享的。直到线程运算结束将值刷新到主内存后,其他线程才可见(可见性).
先举一个场景
public class Test {
static int i = 1;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
i++;
});
Thread t2 = new Thread(() -> {
i++;
});
t1.start();
t2.start();
System.out.println(Test.i);
}
}
这个场景输出有可能不是 3,两个线程从主存中读取 i 的值(1)到各自的高速缓存中,然后线程 t1 执行 +1 操作并将结果写入高速缓存中,最后写入主存中,此时主存 i==2,线程 t2 做同样的操作,但是线程 t2 的工作内存 i 的值有可能还是 1但是现在主存中的 i 应该是 2。所以 t2 执行完最终结果为 2 再将 2 刷新入主内存中而并不是 3。这种现象就是缓存一致性问题。
我们在并发编程中都会知道这几个概念:
- 原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行.
- 可见性: 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值.
- 有序性: 即程序执行的顺序按照代码的先后顺序执行.多线程环境下有影响.
volatile
我们在回顾一下一开始介绍 volatile 的话: volatile 是一个类型修饰符(type specifier).volatile 的作用是确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
看了上面的话只表达出了一点: 可见性.其实还有一个作用就是禁止指令重排序(重排序后面会单独说).所以使用 volatile 关键字来修饰变量就不会存在缓存一致性的问题了.现在我们简单说下 volatile 是怎么禁止指令重排序的: 加入 volatile关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
volatile 就暂且到这里了,感兴趣的朋友可以继续深入研究.volatile 相对于 synchronized 稍微轻量些,在某些场合它可以替代 synchronized,但是又不能完全取代 synchronized.感谢观看!