您的位置:首页 > 产品设计 > UI/UE

漫画:什么是 volatile 关键字? 2017-12-21 玻璃猫 Java编程 来自:程序员小灰(微信号:chengxuyuanxiaohui) ————— 第二天 ————

2017-12-21 11:28 791 查看

挥发性的第一条语义是保证线程间变量的可见性,简单地说就是当线程甲对变量X进行了修改后,在线程甲后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:线程对变量进行修改之后,要立刻回写到主内存。
线程对变量读取的时候,要从主内存中读,而不是缓存。

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
Java的内存模型长成什么样子呢就是下图的样子?

这里需要解释几个概念:
1.主内存(主内存)
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。
2.工作内存(Working Memory)
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

以上说的这些可能有点抽象,大家来看看下面这个例子:
对于一个静态变量 static int s = 0;
线程一个执行如下代码:s = 3;
那么,JMM的工作流程如下图所示:
通过一系列内存读写的操作指令(JVM内存模型共定义了8种内存操作指令,以后会细讲),线程A把静态变量s = 0从主内存读取工作内存,再把s = 3的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。

这时候我们引入线程B,执行如下代码:
System.out.println(“s =”+ s);

引入线程乙以后,当线程甲首先执行,更大的可能是出现下面情况:

此时线程B从主内存得到的值是3,理所当然输出s = 3,这种情况不难理解。但是,有较小的几率出现​​另一种情况:

因为工作内存所更新的变量并不会立即同步到主内存,所以虽然线程甲在工作内存当中已经把变量小号的值更新成如图3所示,但是线程乙从主内存得到的变量小号的值仍然是0,从而输出s = 0。

挥发性关键字具有许多特性,其中最重要的特性就是保证了用挥发性的修饰对变量所有线程的可见性。

这里的可见性是什么意思呢?当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
为什么挥发性关键字可以有这样的特性这得益于Java的语言的?先行发生原则(之前发生)先行发生原则在维基百科上的定义如下:
在计算机科学中,发生在前的关系是两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映出,即使这些事件实际上是乱序执行的(通常是优化程序流程)。 
翻译结果如下:
在计算机科学中,先行发生原则是两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程) 。
这里所谓的事件,实际上就是各种指令操作,比如读操作,写操作,初始化操作,锁操作等等。

先行发生原则作用于很多场景下,包括同步锁,线程启动,线程终止,易失性我们这里只列举出挥发性相关的规则:
对于一个易失性变量的写操作先行发生于后面对这个变量的读操作。
回到上述的代码例子,如果在静态变量小号之前加上挥发性修饰符:
volatile static int s = 0;
线程一个执行如下代码:s = 3;
这时候我们引入线程B,执行如下代码:System.out.println(“s =”+ s);
当线程A先执行的时候,把s = 3写入主内存的事件必定会先于读取s的事件。所以线程B的输出一定是s = 0。

这段代码是什么意思呢?很简单,开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000。

使用挥发性修饰的变量,为什么并发自增的时候会出现这样的问题呢这是因为计数++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令?
getstatic //读取静态变量(count)iconst_1 //定义常量1iadd // count增加1putstatic //把count结果同步到主内存
虽然每一次执行getstatic的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让count自增了很多次。一来本线程所计算更新的是一个陈旧的计值,自然无法做到线程安全:

因此,什么时候适合用挥发性呢?
1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。

第一条很好理解,就是上面的代码例子第二条是什么意思呢可以看看下面这个场景。?
volatile static int start = 3;volatile static int end = 6;
线程一个执行如下代码:while(start <end){  //做一点事}
线程乙执行如下代码:启动+ = 3;端+ = 3;
这种情况下,一旦在线程A的循环中执行了线程B,开始有可能先更新成6,造成了一瞬间开始==结束,从而跳出而循环的可能性。

几点补充:1.关于volatile的介绍,本文很多内容来自“深入理解Java虚拟机”这本书。有兴趣的同学可以去看看。
2. 本漫画纯属娱乐,还请大家尽量珍惜当下的工作,切勿模仿小灰的行为哦。

- - -结束 - - -●本文编号571,以后想阅读这篇文章直接输入571即可●输入米文章电子杂志目录
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: