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并发包的基础。后续并发编程的学习就可以按照这个路线图来学习了。

Java并发编程:volatile的使用相关推荐

  1. null在java存在的意义何在,Java并发编程——volatile关键字

    一.volatile是什么 volatile是Java并发编程中重要的一个关键字,被比喻为"轻量级的synchronized",与synchronized不同的是,volatile ...

  2. Java并发编程——volatile

    引 volatile可以看成是轻量级的低配版的Synchronized,他主要是作用于共享变量,保证共享变量的可见性.确保共享变量在主内存中一致地准确的更新通知到各个线程,这是Volatile的可见性 ...

  3. Java并发编程 Volatile关键字解析

    volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...

  4. Java并发编程—volatile关键字(保证变量的可见性、有序性机制)

    原文作者:Matrix海子 原文地址:Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程 ...

  5. java并发编程-volatile内存实现和原理

    2019独角兽企业重金招聘Python工程师标准>>> 前面的博文说了java的内存模型,介绍了java内存模型的基础,此篇文章来说一下volatile关键字,这个在并发编程中,占有 ...

  6. Java并发编程-Volatile和Syncronized关键字

    Java并发编程学习分享的目标 了解Java并发编程中常用的工具和类用途与用法 了解Java并发编程工具的实现原理与设计思路 了解并发编程中遇到的常见问题与解决方案 了解如何根据实际情景选择更合适的工 ...

  7. Java并发编程--volatile关键字解析

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...

  8. Java并发编程-volatile

    一. volatite 简述 Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. ...

  9. Java并发编程-volatile关键字介绍

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么 ...

最新文章

  1. iOS开发--Mac下server搭建
  2. MySQL-Binlog解析篇:2 Authenticate
  3. 笔试算法题(55):快速排序实现之三路划分, 三元中值法和插入排序处理小子文件...
  4. 51php绑定多个域名,设置Wordpress站点绑定多域名访问 - Mr.bin的博客
  5. 输入A、B,输出A+B
  6. 在 PostgreSQL 中使用码农很忙 IP 地址数据库
  7. datagrid 什么时候结束编辑_孕吐到底什么时候结束
  8. 全Flash网站和单个Flash作品制作的区别
  9. tomcat服务自动关闭_windows10系统关闭自动更新服务
  10. meta你到底了解多少
  11. C语言学习笔记---结构体数组初始化
  12. antd vue关闭模态对话框_我不能没有的5个Vue.js库
  13. i2c电路电平转换电路
  14. 韩立刚老师《计算机网络》笔记3
  15. 服务器常见问题:服务器使用过程中感觉不流畅、卡顿
  16. C++加载csb文件
  17. Dreamweaver CS6实战手册
  18. Photoshop系列_02简单制作一份海报
  19. 机票预定系统可行性分析
  20. Wireshark抓包定位系统网页响应慢 | 网络工程师甩锅技术

热门文章

  1. c语言编译不了什么情况,c语言编译没错但是无法运行
  2. 关于 $ Super $ $ 和 $ Sub $ $ 的用法
  3. 如何设计一个优秀的向导式界面(Wizard)
  4. ubuntu docker慢_基于docker搭建MulVAL攻击图
  5. 会议室时间预约前端_号外!会议室预约流程为您贴心上线
  6. 笑脸符号怎么存入mysql_让MySql支持Emoji表情存储
  7. 网络计算机 用户名和密码,局域网需要用户名和密码怎么办
  8. java 编写 欢迎你_社团联合会——java编程协会欢迎你的加入
  9. linux系统安装金蝶_linux 操作系统安装配置vnc
  10. win7php网页显示空白,win7系统ie11打开网页显示空白的解决方法