使用 volatile 关键字保证变量可见性和禁止指令重排序
volatile 概述
volatile 是 Java 提供的一种轻量级的同步机制。相比于传统的 synchronize,虽然 volatile 能实现的同步性要差一些,但开销更低,因为它不会引起频繁的线程上下文切换和调度。
为了更好的理解 volatile 的作用,首先要了解一下 Java 内存模型与并发编程三要素
Java 内存模型
Java 虚拟机规范中定义了 Java 内存模型(Java Memory Model,JMM),用于屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果。
JMM 规定了 Java 虚拟机与计算机内存如何协同工作:一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。注意这里的变量是指实例字段,静态字段,构成数组对象的元素,不包括局部变量和方法参数(因为这是线程私有的),可以简单理解为主内存是 Java 虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。如果堆中的变量在多线程中都被使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。
Java 内存模型中涉及到的概念有:
主内存
Java 虚拟机规定所有的变量都必须在主内存中产生,该内存是线程公有的,为了方便理解,可以认为是堆区。
工作内存
Java 虚拟机中每个线程都有自己的工作内存,该内存是线程私有的,为了方便理解,可以认为是虚拟机栈。
Java 虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。

并发编程三要素
在并发编程中,以下三要素是我们经常需要考虑的:
原子性
原子是世界上最小的单位,具有不可分割性。同理,将一个操作或多个操作视为一个整体,它们是不可再分的,并且要么全部成功,要么全部失败,那么这个操作就具有原子性。
int a = 10; //1
a++; //2
int b = a; //3
a = a + 1; //4
上面这四个语句中只有第 1 个语句是原子操作,将 10 赋值给线程工作内存的变量 a,而语句2(a++),实际上包含了三个操作:
- 读取变量 a 的值
- 对 a 进行加一的操作
- 将计算后的值再赋值给变量 a,而这三个操作无法构成原子操作
对语句 3,4 的分析同理可得这两条语句不具备原子性。
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。举个简单的例子:
// 线程 1 执行的代码
int i = 0;
i = 10;
//线程 2 执行的代码
j = i;
之前在 Java 内存模型已经讲过,线程 1 执行 i = 10 时,会先把 i 的初始值加载到自己的工作内存,然后赋值为 10,却没有立即写入到主存当中。此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到自己的工作内存中,注意此时内存当中 i 的值还是 0,那么就会使得 j 的值为 0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。
有序性
程序的执行顺序按照代码的先后顺序执行。有序性从不同的角度来看是不同的,单纯从单线程的角度来看,所有操作都是有序的,但到了多线程就不一样了。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
volatile 保证变量可见性
假如有 A、B 两个线程,主内存有变量 i = 0,A 线程将主内存中的 i 拷贝一份到自己的工作内存,并修改为 i = 1,但并没有立即写回到主内存,什么时候写回主存是不确定的。此时 B 线程也将主内存中的 i 拷贝一份到自己的工作内存,而主内存中的 i 还是 0,并不是预想中的 1,这就可能导致一些问题。
volatile 的一个重要作用就是实现了变量可见性。当一个共享变量被 volatile 修饰,它会保证修改的值会立即更新到主存,当其他线程需要读取时,它会去内存中读取新值。
volatile 不保证原子性
假如有 A、B 两个线程,同时对初始值为 0 的变量 i 做加 1 操作,我们希望最终的结果是 i = 2,但有可能并非如此,假设:
- 线程 A 将共享内存 i = 0 拷贝到自己的工作内存,此时 A 的本地内存中 i = 1,但共享内存的 i 还是 0
- 线程 B 将共享内存 i = 0 拷贝到自己的工作内存,此时 B 的本地内存中 i = 1,但共享内存的 i 还是 0
- 线程 A 完成加 1 操作,此时 A 的本地内存中 i = 1,但共享内存的 i 还是 0,线程 A 将 i = 1 写回到内存
- 线程 B 完成加 1 操作,此时 B 的本地内存中 i = 1,共享内存的 i 已经是 1,线程 B 将 i = 1 写回到内存
- 最终共享内存中 i = 1,并不是我们预期的 i = 2
出现上述问题的原因是 i++ 并不是一个原子性的操作,Java 内存模型只保证了基本读取和赋值是原子性操作。不同线程之间的操作交互执行,可能会出现漏洞。所以使用 volatile 必须具备以下两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
上述两个条件其实就是要保证操作是原子性的。如果希望实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现。synchronized 和 Lock 能保证任一时刻只有一个线程执行该代码块,自然就不存在原子性问题。
volatile 禁止指令重排序
所谓指令重排序,是指计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排。指令重排必须保证最终执行结果和代码顺序执行结果一致。
public void mySort() {
int x = 11; // 1
int y = 12; // 2
x = x + 5; // 3
y = x * x; // 4
}
正常的执行顺序是 1、2、3、4,如果发生指令重排,就有可能会是 2、1、3、4,或者是 1、3、2、4 等等,但不会出现 4、3、2、1 这样的情况,因为处理器在进行重排时,必须考虑到指令之间的数据依赖性。
在单线程下指令重排是没有问题的,但如果是多线程就不一定了,假设主存中有 a,b,x,y 四个变量(保证了可见性),初始值都是 0,有 A、B 两个线程,它们各自顺序执行时操作如下:
- 线程 A
- x = a
- b = 1
- 线程 B
- y = b
- a = 2
无论两个线程之间的操作如何交错,最终结果都是 x = 0,y = 0(不考虑线程 A 走完再到线程 B 的情况,因为这样就和单线程没有差异了)。可如果发生了指令重排,此时它们各自的操作执行顺序可能变为:
- 线程 A
- b = 1
- x = a
- 线程 B
- a = 2
- y = b
这样造成的结果就是 x = 2,y = 1,和上面的不一致了。因此为了防止这种情况,volatile 规定禁止指令重排,从而保证数据的一致性。
使用 volatile 关键字保证变量可见性和禁止指令重排序的更多相关文章
- 关于volatile的可见性和禁止指令重排序的疑惑
在学习volatile语义的可见性和禁止指令重排序的相关测试中,发现并不能体现出禁止指令重排序的特性 实验代码如下 package com.aaron.beginner.multithread.vol ...
- 单例模式+volatile禁止指令重排序
单例模式: 单例,顾名思义就是只能有一个.不能再出现第二个.就如同地球上没有两片一模一样的树叶一样. 在这里就是说:一个类只能有一个实例,并且整个项目系统都能访问该实例. 单例模式共分为两大类: 懒汉 ...
- Volatile禁止指令重排序(三)
Volatile禁止指令重排 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种: 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系 ...
- Java并发编程-线程可见性&线程封闭&指令重排序
一.指令重排序 例子如下: public class Visibility1 { public static boolean ready; public static int number; } pu ...
- synchronized无法禁止指令重排序的证明
package demo.reorder; import java.util.concurrent.ExecutorService; import java.util.concurrent.Execu ...
- Volatile如何保证线程可见性之总线锁、缓存一致性协议
基础知识回顾 下图给出了假想机的基本设计.中央处理单元(CPU)是进行算术和逻辑操作的部件,包含了有限数量的存储位置--寄存器(register),一个高频时钟.一个控制单元和一个算术逻辑单元. 时钟 ...
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- 不得不提的volatile及指令重排序(happen-before)
微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...
随机推荐
- jmeter远程调用
jmeter版本相同 JDK版本1.7以上 脚本文件在所有机器上的路径都一致 修改远程机器jmeter bin目录下的jmeter.properties文件配置:server_port=1001 多个 ...
- Oracle数据库之表与表数据操作
一.SQL语言 SQL语言分为四种,分别是:数据定义语言(DDL).数据操纵语言(DCL).事务控制语言(TCL).数据控制语言(DML). 1.1 数据定义语言(DDL) 建立.修改.删除数据库对象 ...
- 如何成为一位优秀的ScrumMaster
嗨,大家好,我是叶子 背景介绍 目标:为了能更好的适应快速变化的需求和不确定的未来. 部门包含岗位:部门负责人.项目经理.产品经理.开发团队(开发人员.测试人员) 那么这种情况下,我们想转型Scrum ...
- (数据科学学习手札95)elyra——jupyter lab最强插件
本文示例文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 jupyter lab是我最喜欢的编辑器,在过往 ...
- PG-跨库操作-postgres_fdw
接上一篇<PG-跨库操作-dblink>:讲下postgres_fdw的使用:postgres_fdw工作原理详细介绍可以去看下<PostgreSQL指南>第4章: 对FDW特 ...
- 容器云平台No.8~kubernetes负载均衡之ingress-nginx
Ingress 是什么? Ingress 公开了从集群外部到集群内服务的 HTTP 和 HTTPS 路由. 流量路由由 Ingress 资源上定义的规则控制. 可以将 Ingress 配置为服务提供外 ...
- Jmeter(二十三) - 从入门到精通 - JMeter函数 - 上篇(详解教程)
1.简介 在性能测试中为了真实模拟用户请求,往往我们需要让提交的表单内容每次都发生变化,这个过程叫做参数化.JMeter配置元件与前置处理器都能帮助我们进行参数化,但是都有局限性,为了帮助我们能够更好 ...
- HotSpot VM 中的JIT分类
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器.开发人员可以通过如下命令显式指定J ...
- BUUCTF[归纳]sql注入相关题目
这是我自己对于sql注入的部分ctf题型的归纳,均来自buuctf的平台环境. [0CTF 2016]piapiapia 我尝试了几种payload,发现有两种情况. 第一种:Invalid user ...
- 智慧组织(SO)如何敏捷构建?
人类社会正处于千年未有之变局的关键时刻--互联网.大数据.AI和实体经济深度融合,数据正在重新定义世界并重构财富体系."新旧交织.破立并存",数字经济方兴未艾,传统势力逐步淡出.各 ...