作者:Paul | 来源:互联网 | 2023-07-01 06:42
volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java5之后,vo
volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。
volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。
————— 第二天 —————
————————————
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
volatile的实现原理
可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
Java内存模型长成什么样子呢?就是下图的样子:
这里需要解释几个概念:
1.主内存(Main Memory)
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。
2.工作内存(Working Memory)
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
以上说的这些可能有点抽象,大家来看看下面这个例子:
对于一个静态变量
static int s = 0;
线程A执行如下代码:
s = 3;
那么,JMM的工作流程如下图所示:
通过一系列内存读写的操作指令(JVM内存模型共定义了8种内存操作指令,以后会细讲),线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。
这时候我们引入线程B,执行如下代码:
System.out.println(“s=” + s);
引入线程B以后,当线程A首先执行,更大的可能是出现下面情况:
此时线程B从主内存得到的s值是3,理所当然输出 s=3,这种情况不难理解。但是,有较小的几率出现另一种情况:
因为工作内存所更新的变量并不会立即同步到主内存,所以虽然线程A在工作内存当中已经把变量s的值更新成3,但是线程B从主内存得到的变量s的值仍然是0,从而输出 s=0。
volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性。
这里的可见性是什么意思呢?当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
为什么volatile关键字可以有这样的特性?这得益于java语言的先行发生原则(happens-before)。先行发生原则在维基百科上的定义如下:
In computer science, the happened-before relation is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow).
翻译结果如下:
在计算机科学中,先行发生原则是两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。
这里所谓的事件,实际上就是各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。
先行发生原则作用于很多场景下,包括同步锁、线程启动、线程终止、volatile。我们这里只列举出volatile相关的规则:
对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。
回到上述的代码例子,如果在静态变量s之前加上volatile修饰符:
volatile static int s = 0;
线程A执行如下代码:
s = 3;
这时候我们引入线程B,执行如下代码:
System.out.println(“s=” + s);
当线程A先执行的时候,把s = 3写入主内存的事件必定会先于读取s的事件。所以线程B的输出一定是s = 3。
这段代码是什么意思呢?很简单,开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000。
使用volatile修饰的变量,为什么并发自增的时候会出现这样的问题呢?这是因为count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令:
getstatic //读取静态变量(count)
iconst_1 //定义常量1
iadd //count增加1
putstatic //把count结果同步到主内存
虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让count自增了很多次。这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全:
因此,什么时候适合用volatile呢?
1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。
第一条很好理解,就是上面的代码例子。第二条是什么意思呢?可以看看下面这个场景:
volatile static int start = 3;
volatile static int end = 6;
线程A执行如下代码:
while (start
//do something
}
线程B执行如下代码:
start+=3;
end+=3;
这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成6,造成了一瞬间 start == end,从而跳出while循环的可能性。
补充:
关于volatile的介绍,本文很多内容来自《深入理解Java虚拟机》这本书。有兴趣的同学可以去看看。
大型网站架构技术
程序员修炼之道
大型web系统数据缓存设计
基于 Redis 实现分布式应用限流
Cache缓存技术全面解析
京东到家库存系统分析
Nginx 缓存引发的跨域惨案
浅谈Dubbo服务框架
数据库中间件架构 | 架构师之路
MySQL优化精髓
看完本文有收获?请转发分享给更多人
欢迎关注“畅聊架构”,我们分享最有价值的互联网技术干货文章,助力您成为有思想的全栈架构师,我们只聊互联网、只聊架构!打造最有价值的架构师圈子和社区。
长按下方的二维码可以快速关注我们