Volatile
关键字相对来说比较容易看的明白,但是在正式使用 Volatile
之前,我们先熟悉下Java
的内存模型,相信看完这个让你对如何判断哪些场景下使用 Volatile
有一个基本认识。
1. 内存基础知识
1.1. JMM概念
JMM(Java Memory Model,简称JMM)并发设计采用的是共享内存的模型概念,所谓的共享内存模型
,即它线程间的通信总是隐式进行,且整个的过程对外部来说不可见。
为了方便理解,我们比喻将每个线程都有自己私有的一块内存,称之为本地内存,本地内存中存储很多共享变量的副本,并都有对这些副本的 R/W
。
- 1 线程A修改共享变量1 的内容,再将 共享变量1刷入主内存中
- 2 线程B从主内存中读取共享变量1
以上步骤存问题,如果在某一时刻,线程A、线程B需要对共享变量(i=0 进行 +1 操作)
- 假定线程A 执行结果是 1,写入主内存,这时候 主内存中 i 的内容是1
- 而这时候线程B 还不知道 主内存中 i 内容已变更,还拿着自己副本(i=0)去执行 +1 操作,再写入主内存
- 此时,两个线程操作共享内容,期望 i 内容是 2,但是实际得出的结果 是 1,因为 线程B 最后的写操作覆盖主内存的 共享变量
小结,如果线程A 在对 共享变量 进行变更时,线程B 需要对修改后的变量内容,可以看见,这就是 Volatile
特点之一 可见性,此外它还有一个重要特性有序性。
在并发编程领域中,一共有三个特性:原子性、可见性、有序性,而 Volatile
已经实现了两种特性。
1.2. 内存模型
1.2.1. 程序计数器
线程私有的,互不影响、独立存储,是当前线程执行的字节码的行号指示器。让字节码解释器工作时可以通过改变这个计数器的值来选取下一执行的字节码的指令,分支、循环、跳转、异常、恢复都需要这个计数器来完成。
程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域。
1.2.2. 虚拟机栈
线程私有的,JAVA方法执行的动态内存模型,每个方法执行都会创建一个栈帧,伴随着方法创建到执行完成,用于存储局部变量表、操作数栈、动态链接、方法出口信息等组成。
局部变量表:局部变量表存放编译器可知的基础类型(包括int、boolean、byte、char、short、lang、double、float)以及对象引用(地址引用或者句柄引用)。大小在编译过程中以及分配,在方法执行中改变不了大小
虚拟机栈常见的两种异常有:
- StackOverFlowError:Java虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机定义的空间,则抛出
StackOverFlowError
- OutOfMemoryError:Java虚拟机栈的内存大小许动态扩展,当线程请求栈的内存用完啦,则抛出
OutOfMemoryError
- StackOverFlowError:Java虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机定义的空间,则抛出
1.2.3. 本地方法栈
与 虚拟机栈
类似,只不过它是虚拟机用到的 Native
方法服务的,也有栈帧,同样的也会抛出与 虚拟机栈
一样的异常。
1.2.4. 堆
这是虚拟机所管理的内存中最大的一块,所有对象实例均存储在这块区域,因此也被人称作GC堆,也是垃圾回收器重点工作的区域。分为:新生代、老年代。
1.2.5. 方法区(JDk1.7)
存储虚拟机加载类信息,常量,静态变量,即编译器编译后代码等数据
- 类的版本
- 字段
- 方法
- 接口
1.2.5.1. 方法区与永久代
1.2.5.2. 垃圾回收在方法区的行为
1.2.5.3. 异常的定义
1.3. 指令重排
指令重排是指在程序执行过程中, 为提高性能考虑,,编译器和CPU可能会对指令重新排序。
举个栗子:
1 | 1 int x = 10; |
理想情况下执行顺序应该是:1>2>3,但是经过JVM等优化侯,顺序会变成:2>1>3。
2. 什么是CAS?
在计算机科学中,实现多线程同步的原子指令特指比较和交换(Conmpare And Swap)
,它存在目的将内存中位置的内容与给定值进行比较。
只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)