并发编程之volatile详解


概述

保证被 volatile 修饰的共享变量的 可见性 和 有序性,但不保证原子性

1.内存模型概述

每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

以 i = i + 1 ;为例,当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

可能存在下面一种情况:初始时,两个线程分别读取 i 的值存入各自所在的CPU的高速缓存当中,然后线程A进行加1操作,然后把i的最新值 1 写入到内存。此时线程 B 的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决这个问题

  • 通过在总线加LOCK#锁的方式
  • 通过缓存一致性协议

2.Java内存模型

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

概念 定义 备注
原子性 一个操作/一系列操作 要么全部执行/全部不执行
可见性 当一个线程修改了共享变量的值后,其他线程能够立刻看到共享变量值的修改 JMM的内存空间分为:主存和工作内存(变量存于主存中;线程使用的是自身工作内存,更新后再同步到主存)。共享属性的修改过程:从主存中读取&复制到工作内存中,在工作内存中修改完成后,再同步到主存、从而刷新主存中的值。若线程A在工作内存中修改完成但还未刷新主存中的值,线程B看到的值还是旧值。此时,可见性就无法保证
有序性 程序的运行顺序看起来 和 我们编写的逻辑的顺序一致,但计算机在实际执行中却不一定 为了提高性能,编译器&处理器都会对代码进行重新排序。但有个前提:重新排序的结果要和单线程执行程序顺序一致

3.保证可见性

volatile修饰的属性保证每次读取都能读到最新的值,但不会 并且 无法更新已经读了的值

线程A在工作内存中修改的共享属性值会立即刷新到主存,线程B/C/D每次通过读写屏障来达到类似于直接从主存中读取属性值

  • 只是类似,网上有些说volatile修饰的变量读写直接在主存中操作,这种说法是不对的,只是表现出这种效果
  • 读写屏障是一条CPU指令;插入一个读写屏障 = 告诉CPU & 编译器先于这个命令的必须先执行,后于这个命令的必须后执行(有序性)
  • 读写屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,以此保证可见性

4.保证有序性

当对volatile修饰的变量进行读/写操作时,其前面的代码必须已执行完成 & 结果对后续的操作可见

原理:重排序时,以volatile修饰变量的读/写操作代码行为分界线,读/写操作前面的代码不许排序到后面,后面同理不许排序到前面。由此保证有序性

5.不保证原子性

volatile修饰的属性若在修改前已读取了值,那么修改后,无法改变已经复制到工作内存的值,即无法阻止并发的情况

6.volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

7.volatile的原理和实现机制

下面这段话摘自《深入理解Java虚拟机》:

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令

lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
  2. 它会强制将对缓存的修改操作立即写入主存
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效

文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录