上一篇文章, 学习了并发编程中的 synchronized, 这个比较好理解, 也是我最初学习多线程编程中的一个简单的实现的, 大学的时候就会了, 然后就一直以为多线程环境的同步只能通过这个来实现的, 事实上 Java 还提供了另外一个更加轻量级的实现 - volatile, 如果说 synchronized 实现了数据在同一时刻只能有一个线程对数据访问的话, 那么 volatile 实现的就是同时可以多个线程在访问数据, 但是只要数据发生了变化, 便确保其他线程及时 "感知" 这种变化.
1,CPU, 主存及高速缓存的概念
计算机的硬件组成可以抽象为由总线, IO 设备, 主存, 处理器 (CPU) 等组成. 其中数据存放在主存中, CPU 负责指令的执行, CPU 的指令执行非常快, 大部分简单指令的执行只需要一个时钟周期, 而一次主内存数据的读取则需要几十到几百个时钟周期, 那么 CPU 从主存中读写数据就会有很大的延迟. 这个时候就产生了高速缓存的概念.
也就是说, 当程序在运行过程中, 会将运算需要的数据从主存复制一份到 CPU 的高速缓存当中, 那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据, 当运算结束之后, 再将高速缓存中的数据回写到主存当中, 通过这种方式来降低 CPU 从主存中获取数据的延迟. 大致的示意图如下:
图一这个模型, 可以简单的认为是单核模型, 在这个模型里面, 以 i++ 这个操作为例, 程序执行时, 会先从主内存中获取 i 的值, 复制到高速缓存, 然后 CPU 从高速缓存中加载并执行 + 1 操作, 操作完成后回写到高速缓存, 最后再从高速缓存回写到主内存. 单核模型这样操作没有任何问题, 但是计算机自产生以来, 一直追求的两个目标, 一个是如何做的更多, 另一个就是如何计算得更快, 这样带来的变化就是单核变成多核, 高速缓存分级存储. 大致的示意图如下:
在图二示意图里面, i++ 这个操作就有问题了, 因为多核 CPU 可以线程并行计算, 在 Core 0 和 Core 1 中可以同时将 i 复制到各自缓存中, 然后 CPU 各自进行计算, 假设初始 i 为 1, 那么预期我们希望是 2, 但是实际由于两个 CPU 各自先后计算后最终主内存中的 i 可能是 2, 也可能是其他值.
这个就是硬件内存架构中存在的一个问题, 缓存一致性问题, 就是说核 1 改变了变量 i 的值之后, 核 0 是不知道的, 存放的还是旧值, 最终对这样的一个脏数据进行操作.
为此, CPU 的厂商定制了相关的规则来解决这样一个硬件问题, 主要有如下方式:
1) 总线加锁, 其实很好理解总线锁, 咱们来看图二, 前面提到了变量会从主内存复制到高速缓存, 计算完成后, 会再回写到主内存, 而高速缓存和主内存的交互是会经过总线的. 既然变量在同一时刻不能被多个 CPU 同时操作, 会带来脏数据, 那么只要在总线上阻塞其他 CPU, 确保同一时刻只能有一个 CPU 对变量进行操作, 后续的 CPU 读写操作就不会有脏数据. 总线锁的缺点也很明显, 有点类似将多核操作变成单核操作, 所以效率低;
2) 缓存锁, 即缓存一致性协议, 主要有 MSI,MESI,MOSI 等, 这些协议的主要核心思想: 当 CPU 写数据时, 如果发现操作的变量是共享变量, 即在其他 CPU 中也存在该变量的副本, 会发出信号通知其他 CPU 将该变量的缓存行置为无效状态, 因此当其他 CPU 需要读取这个变量时, 发现自己缓存中缓存该变量的缓存行是无效的, 那么它就会从内存重新读取.
2,Java 内存模型
在 Java 虚拟机规范中试图定义一种 Java 内存模型 (Java Memory Model,JMM) 来屏蔽各个硬件平台和操作系统的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致的内存访问效果. 在此之前, 主流程序语言 (C/C++ 等) 直接使用物理硬件和操作系统的内存模型(可以理解为类似于直接使用了硬件标准), 都或多或少的在不同的平台有着不一样的执行结果.
Java 内存模型的主要目标是定义程序中各个变量的访问规则, 即变量在内存中的存储和从内存中取出变量这样的底层细节. 其规定了所有变量都存储在主内存, 每个线程还有自己的工作内存, 线程读写变量时需先复制到工作内存, 执行完计算操作后再回写到主内存, 每个线程还不能访问其他线程的工作内存. 大致示意图如下:
图三我们可以理解为和图二表达的是一个意思, 工作内存可以看成是 CPU 高速缓存, 寄存器的抽象, 主内存可以看成就是物理硬件中主内存的抽象, 图二这个模型会存在缓存一致性问题, 图三同样也会存在缓存一致性问题.
另外, 为了获得较好的执行性能, Java 内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度, 也没有限制编译器对指令进行重排序. 也就是说, 在 Java 内存模型中, 还会存在指令重排序的问题.
Java 语言又是怎么来解决这两个问题的呢? 就是通过 volatile 这个关键字来解决缓存一致性和指令重排问题, volatile 作用就是确保可见性和禁止指令重排.
3,volatile 背后实现
那么 volatile 又是怎样来确保的可见性和禁止指令重排呢? 咱们先来写一段单例模式代码来看看.
- public class Singleton {
- private static volatile Singleton instance;
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- public static void main(String[] args) {
- Singleton.getInstance();
- }
- }
先看看字节码层面, JVM 都做了什么.
图四
从图四可以看出, 没有什么特别之处. 既然在字节码层面我们看不出什么端倪, 那下面就看看将代码转换为汇编指令能看出什么端倪. 转换为汇编指令, 可以通过 - XX:+PrintAssembly 来实现, window 环境具体如何操作请参考此处(https://dropzone.nfshost.com/hsdis.xht). 不过比较可惜的是我虽然编译成功了 hsdis-i386.dll(图五), 放置在了 JDK8 下的多个 bin 目录, 一致在报找不到这个 dll 文件所以我决定换个思路一窥究竟.
图五
这个思路就是去阅读 openJDK 的源代码. 其实通过 javap 可以看到 volatile 字节码层面有个关键字 ACC_VOLATILE, 通过这个关键字定位到 accessFlags.hpp 文件, 代码如下:
bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; }
再搜索关键字 is_volatile, 在 bytecodeInterpreter.cpp 可以看到如下代码:
- //
- // Now store the result
- //
- int field_offset = cache->f2_as_index();
- if (cache->is_volatile()) {
- if (tos_type == itos) {
- obj->release_int_field_put(field_offset, STACK_INT(-1));
- } else if (tos_type == atos) {
- VERIFY_OOP(STACK_OBJECT(-1));
- obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
- OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj>> CardTableModRefBS::card_shift], 0);
- } else if (tos_type == btos) {
- obj->release_byte_field_put(field_offset, STACK_INT(-1));
- } else if (tos_type == ltos) {
- obj->release_long_field_put(field_offset, STACK_LONG(-1));
- } else if (tos_type == ctos) {
- obj->release_char_field_put(field_offset, STACK_INT(-1));
- } else if (tos_type == stos) {
- obj->release_short_field_put(field_offset, STACK_INT(-1));
- } else if (tos_type == ftos) {
- obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
- } else {
- obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
- }
- OrderAccess::storeload();
- }
在这段代码中, 会先判断 tos_type, 后面分别有不同的基础类型的实现, 比如 int 就调用 release_int_field_put,byte 就调用 release_byte_field_put 等等. 以 int 类型为例, 继续搜索方法 release_int_field_put, 在 oop.hpp 可以看到如下代码:
void release_int_field_put(int offset, jint contents);
这段代码实际是内联 oop.inline.hpp, 具体的实现是这样的:
inline void oopDesc::release_int_field_put(int offset, jint contents) { OrderAccess::release_store(int_field_addr(offset), contents); }
其实看到这, 可以看到上一篇文章很熟悉的 oop.hpp 和 oop.inline.hpp, 就是很熟悉的 Java 对象模型. 继续看 OrderAccess::release_store, 可以在 orderAccess.hpp 找到对应的实现方法:
static void release_store(volatile jint* p, jint v);
实际上这个方法的实现又有很多内联的针对不同的 CPU 有不同的实现的, 在 src/os_cpu 目录下可以看到不同的实现, 以 orderAccess_linux_x86.inline.hpp 为例, 是这么实现的:
inline void OrderAccess::release_store(volatile jint* p, jint v) { *p = v; }
可以看到其实 Java 的 volatile 操作, 在 JVM 实现层面第一步是给予了 C++ 的原语实现, 接下来呢再看 bytecodeInterpreter.cpp 截取的代码, 会再给予一个 OrderAccess::storeload()操作, 而这个操作执行的代码是这样的(orderAccess_linux_x86.inline.hpp):
inline void OrderAccess::storeload() { fence(); }
fence 方法代码如下:
- inline void OrderAccess::fence() {
- if (os::is_MP()) {
- // always use locked addl since mfence is sometimes expensive
- #ifdef AMD64
- __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
- #else
- __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
- #endif
- }
- }
一样可以看到和通过 - XX:+PrintAssembly 来看到的背后实现: lock; addl, 其实这个就是内存屏障, 关于内存屏障的详细说明可以看下 orderAccess.hpp 的注释. 内存屏障提供了 3 个功能: 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 也不会把前面的指令排到内存屏障的后面; 强制将对缓存的修改操作立即写入主存; 如果是写操作, 它会导致其他 CPU 中对应的缓存行无效. 这 3 个功能又是怎么做到的呢? 来看下内存屏障的策略:
在每个 volatile 写操作前面插入 storestore 屏障;
在每个 volatile 写操作后面插入 storeload 屏障;
在每个 volatile 读操作后面插入 loadload 屏障;
在每个 volatile 读操作后面插入 loadstore 屏障;
其中 loadload 和 loadstore 对应的是方法 acquire,storestore 对应的是方法 release,storeload 对应的是方法 fence.
4,volatile 应用场景
4.1 double check 单例
- public class Singleton {
- private static volatile Singleton instance;
- private Singleton() {};
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
为什么要这样写, 这个网上有很多资料, 这里就不赘述了.
4.2 java.util.concurrent
大量的应用在 j.u.c 下的各个基础类和工具栏, 构成 Java 并发包的基础. 后续并发编程的学习就可以按照这个路线图来学习了.
参考资料:
- https://github.com/lingjiango/ConcurrentProgramPractice
- https://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java
- https://stackoverflow.com/questions/106591/do-you-ever-use-the-volatile-keyword-in-java
- https://www.cnblogs.com/zhangj95/p/5647051.html
- http://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec
- https://www.cs.umd.edu/~pugh/java/memoryModel/
来源: https://www.cnblogs.com/iou123lg/p/9280639.html