NightPxy 个人技术博客

java-volatile

Posted on By NightPxy

概述

volatile 是个人觉得最奇妙的关键字
首先它是使用起来最简单的关键字,它的意义是强制赋值必定写入主内存,读取必定从主内存加载
然而它又是理解相当困难的关键字,因为它牵涉到java的内存模型

高速缓存带来的问题

首先说一点关于程序执行向的东西
程序的执行是在CPU中.如果牵涉到数据的读取和写入,又必然会与系统内存打交道.
这个过程非常简单,也没有任何问题.问题仅仅在,这样根本行不通.
行不通的原因非常简单,就是内存太慢了,速度跟不上CPU的执行速度.
所以引入一个机制叫做高速缓存.volatile解决问题实质上就是解决高速缓存带来的问题

高速缓存在尽可能靠近CPU的位置(甚至有一级二级N级之分),所以变成了这样
CPU <-> 高速缓存 <-> 主内存
问题就出在这里.高速缓存和主内存是分开之后一级一级过去的,这样在多线程情况下,是很可能出现线程安全问题的(高速缓存和主内存不一致)

最早解决这个问题的思路是锁.很容易就能推测出这样行不通,如果主内存连串行都能接受,那何必引入高速缓存呢
最终解决这个问题的办法,就是缓存一致性
缓存一致性非常简单,它允许有多个副本并且初始一致,如果其中一份副本发生改变,就会立即通知其它副本无效

并发编程的三个概念

  • 原子性
    这个非常好理解,就是其中不可分割,要么全部成功要么全部失败
  • 可见性
    可见性是指多个线程访问一个变量,如果这个变量发生变化.每个线程都能始终看见变化后的值
  • 有序性
    有序性是指程序执行的顺序始终按照代码顺序执行

有序性与指令重排
举个例子说

int a=1
int b=2
int c=a*b

这段代码中的执行顺序一定是a前b后么.答案是不一定的,这里有个特别的机制叫做指令重排
指令重排比较抽象,但其实与kafka全局无序性非常类似.多核CPU,每个CPU就相当于Kafka分区
上个指令处理过长等等各种情况,每个分区执行的速度都不一致,所以CPU指令执行总体上呈现出混乱无序的情况.

但如果指令重排,程序很可能出现C出错.但事实上,无论怎么执行这段代码都是没问题的.
没有问题的原因在于有另外的机制保障了这段代码指令的有序性,这个保障机制就叫做内存栅栏
内存栅栏的机制相对比较复杂.大体上的意思是指令重排时承诺一定条件下(比如指令存在依赖关系),必然不会出现重排(承诺存在依赖的关系的指令必定进入同一个CPU,这样就是有序的).

内存栅栏规则如下

  • LoadLoad 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载
  • StoreStore 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储
  • LoadStore 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存
  • StoreLoad 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

综上,程序并发执行的安全,原子性,可见性和有序性三者缺一不可

java内存模型

前面说的都是操作系统概念,类似JVM规范,下面来看看作为实现的java如何提供保障的

首先java内存模型规定所有变量都必须存于主内存中
其次java内存模型为每个线程规定了自己的工作内存(对标高速缓存),并同时规定线程必须只对自己的工作内存工作,即线程只能读取不许修改主内存,并且不允许访问其它线程的工作内存
比如

int a =1
//这句话实际执行是,首先对线程自己的工作内存变量赋值1,然后同步入主内存,而不是直接在主内存中赋值1

原子性

java内存模型承诺,对所有基本类型变量的读取和赋值是原子性的

int a=1 //原子性的  
int b=a //非原子性的,因为这实际是两步操作,读取a,赋值b 
a++ //同理非原子性
x=x+1 //同理非原子性  

可见性

java内存模型对可见性是可选性的,这就是volatile关键字的由来 它的意义在于保证对变量的更新会立即更新到主内存,并保证读取时始终从主内存中读取,如果没有这个关键字修饰,则不一定立即写入主内存,这样其它线程就有可能读取到未修改的值

除了volatile之外,还有以下途径可以保证可见性

  • synchronized,Lock同步块
    同步块保证可见性是同步块内部有一条规则是解除同步那一刻必须先将变量写入主内存
  • final final关键字保证可见性是final关键字修饰的不可变变量,在构造方法一旦赋值完毕也会立即写入主内存(this引用逃逸情况除外)

有序性

Java中有 happens-before 原则,用以保证多线程操作之间的可见性

  • 程序顺序规则
    一个线程中的每个操作,happens- before 于该线程中的任意后续操作
  • 监视器锁规则
    对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁
  • volatile变量规则
    对一个volatile域的写,happens- before 于任意后续对这个volatile域的读
  • 传递性
    如果A happens- before B,且B happens- before C,那么A happens- before C