若是一个线程对共享变量值的修改, 可以及时的被其余线程看到, 叫作共享变量的可见性.缓存
Java 虚拟机规范试图定义一种 Java 内存模型 (JMM), 来屏蔽掉各类硬件和操做系统的内存访问差别, 让 Java 程序在各类平台上都能达到一致的内存访问效果. 多线程
简单来讲, 因为 CPU 执行指令的速度是很快的, 可是内存访问的速度就慢了不少, 相差的不是一个数量级, 因此搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存.并发
在 Java 内存模型里, 对上述的优化又进行了一波抽象. JMM 规定全部变量都是存在主存中的, 相似于上面提到的普通内存, 每一个线程又包含本身的工做内存, 方便理解就能够当作 CPU 上的寄存器或者高速缓存. app
因此线程的操做都是以工做内存为主, 它们只能访问本身的工做内存, 且工做先后都要把值在同步回主内存.优化
简单点就是, 多线程中读取或修改共享变量时, 首先会读取这个变量到本身的工做内存中成为一个副本, 对这个副本进行改动后, 再更新回主内存中.spa
使用工做内存和主存, 虽然加快的速度, 可是也带来了一些问题. 好比看下面一个例子:操作系统
i = i + 1;
假设 i
初值为 0
, 当只有一个线程执行它时, 结果确定获得 1
, 当两个线程执行时, 会获得结果 2
吗? 这倒不必定了. 可能存在这种状况:线程
线程1: load i from 主存 // i = 0 i + 1 // i = 1 线程2: load i from主存 // 由于线程1还没将i的值写回主内存,因此i仍是0 i + 1 //i = 1 线程1: save i to 主存 线程2: save i to 主存
若是两个线程按照上面的执行流程, 那么 i
最后的值竟然是 1
了. 若是最后的写回生效的慢, 你再读取 i
的值, 均可能是 0
, 这就是缓存不一致问题.code
这种状况通常称为 失效数据, 由于线程1 还没将 i
的值写回主内存, 因此 i
仍是 0
, 在线程2 中读到的就是 i
的失效值(旧值).blog
也能够理解成, 在操做完成以后将工做内存中的副本回写到主内存, 而且在其它线程从主内存将变量同步回本身的工做内存以前, 共享变量的改变对其是不可见的.
有序性: 即程序执行的顺序按照代码的前后顺序执行. 举个简单的例子, 看下面这段代码:
int i = 0; boolean flag = false; i = 1; //语句1 flag = true; //语句2
上面代码定义了一个 int
型变量, 定义了一个 boolean
类型变量, 而后分别对两个变量进行赋值操做.
从代码顺序上看, 语句1 是在语句2 前面的, 那么 JVM 在真正执行这段代码的时候会保证语句1 必定会在语句2 前面执行吗? 不必定, 为何呢? 这里可能会发生指令重排序.
重排序
指令重排是指 JVM 在编译 Java 代码的时候, 或者 CPU 在执行 JVM 字节码的时候, 对现有的指令顺序进行从新排序.
它不保证程序中各个语句的执行前后顺序同代码中的顺序一致, 可是它会保证程序最终执行结果和代码顺序执行的结果是一致的(指的是不改变单线程下的程序执行结果).
虽然处理器会对指令进行重排序, 可是它会保证程序最终结果会和代码顺序执行结果相同, 那么它靠什么保证的呢? 再看下面一个例子:
int a = 10; //语句1 int r = 2; //语句2 a = a + 3; //语句3 r = a*a; //语句4
这段代码有 4 个语句, 那么可能的一个执行顺序是:
那么可不多是这个执行顺序呢?
语句2 语句1 语句4 语句3.
不可能, 由于处理器在进行重排序时是会考虑指令之间的数据依赖性, 若是一个指令 Instruction 2 必须用到 Instruction 1 的结果, 那么处理器会保证 Instruction 1 会在 Instruction 2 以前执行.
虽然重排序不会影响单个线程内程序执行的结果, 可是多线程呢? 下面看一个例子:
//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
上面代码中, 因为语句1 和语句2 没有数据依赖性, 所以可能会被重排序.
假如发生了重排序, 在线程1 执行过程当中先执行语句2, 而此时线程2 会觉得初始化工做已经完成, 那么就会跳出 while
循环, 去执行 doSomethingwithconfig(context)
方法, 而此时 context
并无被初始化, 就会致使程序出错.
从上面能够看出, 指令重排序不会影响单个线程的执行, 可是会影响到线程并发执行的正确性.
Java 中, 对基本数据类型的读取和赋值操做是原子性操做, 所谓原子性操做就是指这些操做是不可中断的, 要作必定作完, 要么就没有执行.
JMM 只实现了基本的原子性, 像 i++
的操做, 必须借助于 synchronized
和 Lock
来保证整块代码的原子性了. 线程在释放锁以前, 必然会把 i
的值刷回到主存的.
重点, 要想并发程序正确地执行, 必需要保证原子性、可见性以及有序性. 只要有一个没有被保证, 就有可能会致使程序运行不正确.
volatile 关键字的两层语义
一旦一个共享变量 (类的成员变量、类的静态成员变量) 被 volatile
修饰以后, 那么就具有了两层语义:
1) 禁止进行指令重排序.
2) 读写一个变量时, 都是直接操做主内存.
在一个变量被 volatile
修饰后, JVM 会为咱们作两件事:
1.在每一个 volatile
写操做前插入 StoreStore
屏障, 在写操做后插入 StoreLoad
屏障.
2.在每一个 volatile
读操做前插入 LoadLoad
屏障, 在读操做后插入 LoadStore
屏障.
或许这样说有些抽象, 咱们看一看刚才线程A代码的例子:
boolean contextReady = false; //在线程A中执行: context = loadContext(); contextReady = true;
咱们给 contextReady
增长 volatile
修饰符, 会带来什么效果呢?
因为加入了 StoreStore
屏障, 屏障上方的普通写入语句 context = loadContext()
和屏障下方的 volatile
写入语句 contextReady = true
没法交换顺序, 从而成功阻止了指令重排序.
也就是说, 当程序执行到 volatile
变量的读或写操做时, 在其前面的操做的更改确定所有已经进行, 且结果已经对后面的操做可见.
volatile特性之一:
保证变量在线程之间的可见性. 可见性的保证是基于 CPU 的内存屏障指令, 被 JSR-133 抽象为 happens-before
原则.
volatile特性之二:
阻止编译时和运行时的指令重排. 编译时 JVM 编译器遵循内存屏障的约束, 运行时依靠 CPU 屏障指令来阻止重排.
volatile
除了保证可见性和有序性, 还解决了long
类型和double
类型数据的 8 字节赋值问题.
虚拟机规范中容许对 64 位数据类型, 分为 2 次 32 位的操做来处理, 当读取一个非volatile
类型的 long 变量时, 若是对该变量的读操做和写操做不在同一个线程中执行, 那么颇有可能会读取到某个值得高 32 位和另外一个值得低 32 位.