多线程系列(四) -volatile关键字使用详解
一、简介
在上篇文章中,我们介绍到在多线程环境下,如果编程不当,可能会出现程序运行结果混乱的问题。
出现这个原因主要是,JMM 中主内存和线程工作内存的数据不一致,以及多个线程执行时无序,共同导致的结果。

同时也提到引入synchronized同步锁,可以保证线程同步,让多个线程依次排队执行被synchronized修饰的方法或者方法块,使程序的运行结果与预期一致。
不可否认,采用synchronized同步锁确实可以保证线程安全,但是它对服务性能的消耗也很大,synchronized是一个独占式的同步锁,比如当多个线程尝试获取锁时,其中一个线程获取到锁之后,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,当冲突严重的时候,线程会直接进入阻塞状态,不能再干别的活。
为了实现线程之间更加方便的访问共享变量,Java 编程语言还提供了另一种同步机制:volatile域变量,在某些场景下使用它会更加方便。
一般来说,被volatile修饰的变量,可以保证所有线程看到这个变量都是同一个值,同时它不会引起线程上下文的切换和调度,相比synchronized,volatile更加的轻量化。
比较官方的解释,volatile修饰变量有以下几个作用:
1.保证变量的可见性,不保证原子性
当用volatile修饰一个变量时,JMM 会把当前线程本地内存中的变量强制刷新到主内存中去,这个写操作也会导致其他线程中被volatile修饰的变量缓存无效,然后从主内存中获取最新的值2.禁止指令重排
正常情况下,编译器和处理器为了优化程序执行性能会对指令序列进行重排序,当然是在不影响程序结果的前提下。volatile能够在一定程度上禁止 JVM 进行指令重排。
从概念上感觉比较难理解,下面我们结合几个例子,一起来看看它的具体应用。
二、volatile 使用详解
我们先看一个例子。
public class DataEntity {
private boolean isRunning = true;
public void addCount(){
System.out.println("线程运行开始....");
while (isRunning){ }
System.out.println("线程运行结束....");
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
}
public class MyThread extends Thread {
private DataEntity entity;
public MyThread(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount();
}
}
public class MyThreadTest {
public static void main(String[] args) throws InterruptedException {
// 初始化数据实体
DataEntity entity = new DataEntity();
MyThread threadA = new MyThread(entity);
threadA.start();
// 主线程阻塞1秒
Thread.sleep(1000);
// 将运行状态设置为false
entity.setRunning(false);
}
}
运行结果如下:

从实际运行结果来看,程序进入死循环状态,虽然最后一行手动设置了entity.setRunning(false),但是没有起到任何的作用。
原因其实也很简单,虽然主线程main将isRunning变量设置为false,但是线程threadA 里面的isRunning变量还是true,两个线程看到的数据不一致。
假如在isRunning变量上,加一个volatile关键字,我们再来看看运行效果。
/**
* 在 isRunning 变量上加一个 volatile 关键字
*/
private volatile boolean isRunning = true;
运行结果如下:

程序运行后自动结束。
说明当主线程main将isRunning变量设置为false时,线程threadA 里面的isRunning值也随着发生变化。
说明被volatile修饰的变量,在多线程环境下,可以保证所有线程看到这个变量都是同一个值。
三、volatile 不适用的场景
对于某些场景下,volatile可能并不适用,我们还是先看一个例子。
public class DataEntity {
private volatile int count = 0;
public void addCount(){
for (int i = 0; i < 100000; i++) {
count++;
}
}
public int getCount() {
return count;
}
}
public class MyThreadTest {
public static void main(String[] args) throws InterruptedException {
// 初始化数据实体
DataEntity entity = new DataEntity();
// 初始化5个线程计数器
CountDownLatch latch = new CountDownLatch(5);
// 采用多线程进行操作
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
entity.addCount();
//线程运行完毕减1
latch.countDown();
}
}).start();
}
// 等待以上线程执行完毕,再获取结果
latch.await();
System.out.println("result: " + entity.getCount());
}
}
运行结果如下:
第一次运行:result: 340464
第二次运行:result: 318342
第三次运行:result: 305957
理论上使用 5 个线程分别执行了100000自增,我们预期的结果应该是5*100000=500000,从实际的运行结果可以看出,与预期不一致。
这是因为volatile的作用其实是有限的,它只能保证多个线程之间看到的共享变量值是最新的,但是无法保证多个线程操作共享变量时依次有序,无法保证原子性操作。
上面的例子中count++不是一个原子性操作,在处理器看来,其实一共做了三个步骤的操作:读取数据、对数据加 1、回写数据,在多线程随机执行情况下,输出结果不能达到预期值。
如果想要实现与预期一致的结果,有以下三种方案可选。
方案一:采用synchronized同步锁
public class DataEntityC2 {
private int count = 0;
/**
* 采用 synchronized 同步锁,可以实现多个线程执行方法时串行
*/
public synchronized void addCount(){
for (int i = 0; i < 100000; i++) {
count++;
}
}
public int getCount() {
return count;
}
}
方案二:采用Lock锁
public class DataEntityC2 {
private int count = 0;
private Lock lock = new ReentrantLock();
/**
* 采用 Lock 锁,可以实现多个线程执行方法时串行
*/
public void addCount(){
for (int i = 0; i < 100000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
public int getCount() {
return count;
}
}
方案三:采用JUC包中的原子操作类
public class DataEntity {
private AtomicInteger inc = new AtomicInteger();
/**
* 采用原子操作类,原子操作类是通过CAS循环的方式来保证操作原子性
*/
public void addCount(){
for (int i = 0; i < 100000; i++) {
inc.getAndIncrement();
}
}
public int getCount() {
return inc.get();
}
}
以上三种方案,都可以实现程序的运行结果与预期一致!
四、volatile 的原理
通过以上的例子介绍,相信大家对volatile关键字的作用有了一些认识。
volatile修饰的变量,可以保证变量在内存中的可见性,但是无法保证原子性操作。
关于原子性、可见性和有序性的定义,这三个特性主要从多线程编程安全角度总结出来的一些基本要素,也是并发编程的三大核心基础,在上篇文章中有所提到过,这里不再重复讲了。
在 JVM 底层,volatile是通过采用“内存屏障”来实现内存可见性和禁止指令重排。观察不加入volatile和加入volatile关键字所生成的汇编代码发现,加入volatile关键字的代码会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,可以提供以下 3 个功能。
- 1.它确保指令重排序时,不会把后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,禁止处理器对影响程序执行结果的指令进行重排
- 2.它会强制将缓存的修改操作立刻写入主存,保证内存变量可见
- 3.如果是写操作,它会导致其它 CPU 中对应的行缓存无效,目的是让其他线程中被
volatile修饰的变量缓存无效,然后从主内存中获取最新的值
五、单例模式中的双重检锁为什么要加 volatile?
在上篇文章中,我们提到过单例设计模式中的双重校验锁实现。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { //第一行
synchronized (Singleton.class) { //第二行
if (singleton == null) { //第三行
singleton = new Singleton(); //第四行
}
}
}
return singleton; //第五行
}
}
synchronized可以保证原子性、可见性和有序性,为什么变量singleton还需要加volatile关键字呢?
之所以需要加volatile关键字的原因是:问题出在第一行代码不在同步代码块之类,可能出现这个对象地址不为空,但是内容为空。
以初始化一个Singleton singleton = new Singleton();为例,JVM 会分三个步骤完成:
a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时可能会出现重排序,因为b和c无逻辑关联,执行的顺序是a -> b -> c或者a -> c -> b,在多线程的环境下可能会出现问题。
分析过程如下:
- 1.线程 A 执行到第四行代码时,线程 B 进来执行第一行代码
- 2.假设线程 A 在执行过程中发生了指令重排序,先执行了
a和c,没有执行b - 3.由于线程 A 执行了
c导致instance指向了一段地址,此时线程 B 检查singleton发现不为null,会直接跳转到第五行代码,返回一个未初始化的对象,导致程序会出现报错 - 4.因此需要在
singleton变量上加一个volatile关键字,当线程 A 执行完毕b操作之后,会变量强制刷新到主内存中,此时线程 B 也可以拿到最新的对象
这就是为啥双重检锁模式中,singleton变量为啥要加一个volatile关键字的原因。
采用双重检锁的方式,可以显著的提升并发查询的效率。
六、小结
本篇文章主要围绕volatile关键字的用途、使用方式和一些坑点,做了一个简单的知识总结,内容难免有所遗漏,欢迎网友留言指出!
七、参考
多线程系列(四) -volatile关键字使用详解的更多相关文章
- volatile关键字的详解-并发编程的体现
xl_echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!! 参 ...
- SAP ECC6安装系列四:安装过程详解
原作者博客 http://www.cnblogs.com/Michael_z/ ======================================== 续接上篇,我们终于按下了 “Next” ...
- Spring系列(四):Spring AOP详解和实现方式(xml配置和注解配置)
参考文章:http://www.cnblogs.com/hongwz/p/5764917.html 一.什么是AOP AOP(Aspect Oriented Programming),即面向切面编程, ...
- Android Studio系列教程五--Gradle命令详解与导入第三方包
Android Studio系列教程五--Gradle命令详解与导入第三方包 2015 年 01 月 05 日 DevTools 本文为个人原创,欢迎转载,但请务必在明显位置注明出处!http://s ...
- [js高手之路] es6系列教程 - 对象功能扩展详解
第一:字面量对象的方法,支持缩写形式 //es6之前,这么写 var User = { name : 'ghostwu', showName : function(){ return this.nam ...
- 剑指Offer——线程同步volatile与synchronized详解
(转)Java面试--线程同步volatile与synchronized详解 0. 前言 面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现 ...
- SpringBoot系列(十二)过滤器配置详解
SpringBoot(十二)过滤器详解 往期精彩推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配置文件 ...
- ava下static关键字用法详解
Java下static关键字用法详解 本文章介绍了java下static关键字的用法,大部分内容摘自原作者,在此学习并分享给大家. Static关键字可以修饰什么? 从以下测试可以看出, static ...
- Java多线程编程中Future模式的详解
Java多线程编程中,常用的多线程设计模式包括:Future模式.Master-Worker模式.Guarded Suspeionsion模式.不变模式和生产者-消费者模式等.这篇文章主要讲述Futu ...
- 构建安全的Xml Web Service系列之wse之错误代码详解
原文:构建安全的Xml Web Service系列之wse之错误代码详解 WSE3.0现在还没有中文版的可以下载,使用英文版的过程中,难免会遇到各种各样的错误,而面对一堆毫无头绪的错误异常,常常会感到 ...
随机推荐
- 【C++】模板
模板 模板是一种参数化的多态性工具 参数化多态性是指将程序所处理的对象的类型参数化,是一段程序代码可以用于处理多种不同类型的对象 模板是一种使用无类型参数来产生一族函数或类的机制 参数化是指模板不以数 ...
- ZHS16GBK字符集下面Oracle数据库varchar与nvarchar的验证
ZHS16GBK字符集下面Oracle数据库varchar与nvarchar的验证 背景 周末分析了 SQLServer mysql等数据库 想着继续分析一下oracle数据库 这边oracle使用的 ...
- [转帖]Intel AVX 系列指令基础介绍
https://zhuanlan.zhihu.com/p/437657452 一.发展背景 1993年,Intel公司推出了奔腾处理器,该类型处理器拥有两条执行流水线,和当时的处理器相比,可以同时执行 ...
- [转帖]挂载文件系统选项nodiratime、noatime
默认的方式下linux会把文件访问的时间atime做记录,文件系统在文件被访问.创建.修改等的时候记录下了文件的一些时间戳,比如:文件创建时间.最近一次修改时间和最近一次访问时间:这在绝大部分的场合都 ...
- Jmeter学习之四_kingbaseV8R6数据库的简单验证
Jmeter学习之四_kingbaseV8R6数据库的简单验证 背景 周一没去报道, 因为我忘记体检了... 继续在家进行学习提高自己. jmeter周末时开始看的. 今天想着继续研究一下对数据库的处 ...
- [转帖]resin的安装与配置
1.安装jdk 之前装过了,就不做演示了 vim /etc/profile //注意java的配置文件 JAVA_HOME=/usr/local/jdk1.8/ JAVA_BIN=/usr/loc ...
- Windows 环境下简单的自动备份以及清理数据库的操作过程
今天能想到要简单的备份一下windows上面的数据库. 然后并且能够定期清理文件. 然后从网上找了一下 找到把饭如下 1. 备份 创建一个目录用来存放数据库备份 c:\dbbak 然后编写一个脚本, ...
- Jumper Server 堡垒机搭建过程
Jumper Server 堡垒机搭建过程 背景说明 公司组织考核, 要对一套系统进行安全设置.有一个项目是使用堡垒机进行登录 堡垒机有多种用途,可以实现日志审计和安全设置等. 买商业设备的话太困难了 ...
- 京东云开发者|提高IT运维效率,深度解读京东云AIOps落地实践
基于深度学习对运维时序指标进行异常检测,快速发现线上业务问题 时间序列的异常检测是实际应用中的一个关键问题,尤其是在 IT 行业.我们没有采用传统的基于阈值的方法来实现异常检测,而是通过深度学习提出了 ...
- # github突破7k star 即时通讯(IM)开源项目OpenIM每周迭代版本发布
v2.0已经重构完毕,架构更清晰,代码更规范,邀请各位参与OpenIM社区建设有兴趣的同学可以加我私聊. 目前侧正在业务开发,已提供更多功能,包括群管理,阅后即焚,朋友圈,标签下发等. web端体验: ...