• Java 多线程与并发(五):volatile


    除了 Synchronized 关键字,Java 还提供了一个更加轻量级的实现 volatile,在上一章介绍 CAS 时我们也说过,volatile 能够保证多线程环境下的可见性与防止指令重排序带来的问题。

    先从硬件说起

    前面文章已经介绍了硬件结构与 JMM 这里我们自回顾以下。

    因为内存读写的速度远远跟不上 CPU 的运算速度,所以在内存和 CPU 之间加了告诉缓存。

    当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据回写到主存当中,通过这种方式来降低CPU从主存中获取数据的延迟。

    可以简单的认为是单核模型,在这个模型里面,以i++这个操作为例,程序执行时,会先从主内存中获取i的值,复制到高速缓存,然后CPU从高速缓存中加载并执行+1操作,操作完成后回写到高速缓存,最后再从高速缓存回写到主内存。

    但是遇到多核 CPU 就有问题了。i++这个操作就有问题了,因为多核CPU可以线程并行计算,在Core 0和Core 1中可以同时将i复制到各自缓存中,然后CPU各自进行计算,假设初始i为1,那么预期我们希望是2,但是实际由于两个CPU各自先后计算后最终主内存中的i可能是2,也可能是其他值。

    img

    为此,CPU的厂商定制了相关的规则来解决这样一个硬件问题,主要有如下方式:

      1) 总线加锁,其实很好理解总线锁,咱们来看图二,前面提到了变量会从主内存复制到高速缓存,计算完成后,会再回写到主内存,而高速缓存和主内存的交互是会经过总线的。既然变量在同一时刻不能被多个CPU同时操作,会带来脏数据,那么只要在总线上阻塞其他CPU,确保同一时刻只能有一个CPU对变量进行操作,后续的CPU读写操作就不会有脏数据。总线锁的缺点也很明显,有点类似将多核操作变成单核操作,所以效率低;

      2) 缓存锁,即缓存一致性协议,主要有MSI、MESI、MOSI等,这些协议的主要核心思想:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

    再说 JMM

    在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(C/C++等)直接使用物理硬件和操作系统的内存模型(可以理解为类似于直接使用了硬件标准),都或多或少的在不同的平台有着不一样的执行结果。 

    Java内存模型的主要目标是定义程序中各个变量的访问规则,即变量在内存中的存储和从内存中取出变量这样的底层细节。其规定了所有变量都存储在主内存,每个线程还有自己的工作内存,线程读写变量时需先复制到工作内存,执行完计算操作后再回写到主内存,每个线程还不能访问其他线程的工作内存。大致示意图如下:

    img

     另外,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在Java内存模型中,还会存在指令重排序的问题。

    JMM 为我们带来了两个问题,就是线程在自己的工作内存更新变量后其他线程不知道的可见性问题,还有指令重排序问题。

    怎么解决拿?就是今天要将的 volatile 关键字。

    内存屏障

    内存屏障是一组处理指令,用来实现对内存操作的顺序限制。JMM 把内存屏障分为以下四类:

    屏障类型 指令示例 说明
    LoadLoad 屏障 Load1; LoadLoad; Load2 确保 Load1 数据的装载,之前于 Load2 及其所有后续的装载指令的装载
    StoreStore 屏障 Sotre1; SotreSotre; Store2 确保 Store1 数据对其他处理器可见(刷新到主存),之前于 Store2 及其所有后续存储指令的存储
    LoadStore 屏障 Load1; LoadStore; Store2 确保 Load1 数据装载,之前于 Store2 及其后续存储指令刷新到主存
    StoreLoad 屏障 Sotre1; StoreLoad; Load2 确保 Store1 数据对其他处理器变得可见(刷新到主存),之前于 Load2 及其后续装载指令的装载。

    可以通过 happen-before 这个概念来阐述操作之间的可见性,如果一个操作的执行结果需要对另一个可见,那么这两个操作之阿健必须存在 happen-before 关系。

    程序员密切相关的 happen-before 规则如下:

    • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

    • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

    • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。

    • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

    volatile 禁止指令重排序于保证可见性

    重排序:

    • 编译器对 volatile 类型的变量不会再进行优化。
    • volatile 底层通过内存屏障禁止特定类型的处理器重排序。

    可见性:

    通过内存屏障,确保某个线程对 volatile 变量的修改会立即刷新到主存,并且导致其他线程工作内存中的副本无效,读取时只能从主内存加载最新的值。

    比如 volatile store -> volatile load,通过插入 StoreLoad 内存屏障,确保 Store 数据对其他处理器变得可见(刷新到内存),之前于 Load 及其后续所有装载指令的装载。

  • 相关阅读:
    HDU 2104 hide handkerchief
    HDU 1062 Text Reverse 字符串反转
    HDU 1049
    HDU 1096 A+B for Input-Output Practice (VIII)
    POJ 1017
    C/C++一些难为人知的小细节
    小刘同学的第十二篇博文
    小刘同学的第十一篇博文
    小刘同学的第十篇博文
    小刘同学的第九篇日记
  • 原文地址:https://www.cnblogs.com/paulwang92115/p/12167760.html
Copyright © 2020-2023  润新知