前言

不管是在面试还是实际开发中 volatile 都是一个应该掌握的技能。

首先来看看为什么会出现这个关键字。

内存可见性

由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。

线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。

这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存

如下图所示:

所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。

显然这肯定是会出问题的,因此 volatile 的作用出现了:

当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。

volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中

内存可见性的应用

当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:

public class Volatile implements Runnable{

    private static volatile boolean flag = true ;

    @Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName() + "正在运行。。。");
}
System.out.println(Thread.currentThread().getName() +"执行完毕");
} public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start(); System.out.println("main 线程正在运行") ; TimeUnit.MILLISECONDS.sleep(100) ; aVolatile.stopThread(); } private void stopThread(){
flag = false ;
}
}

主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。

但这里有个误区,这样的使用方式容易给人的感觉是:

volatile 修饰的变量进行并发操作是线程安全的。

这里要重点强调,volatile不能保证线程安全性!

如下程序:

public class VolatileInc implements Runnable{

    private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性

    //private static AtomicInteger count = new AtomicInteger() ;

    @Override
public void run() {
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet() ;
}
} public static void main(String[] args) throws InterruptedException {
VolatileInc volatileInc = new VolatileInc() ;
Thread t1 = new Thread(volatileInc,"t1") ;
Thread t2 = new Thread(volatileInc,"t2") ;
t1.start();
//t1.join(); t2.start();
//t2.join();
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet();
} System.out.println("最终Count="+count);
}
}

当我们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

  • 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。

  • 也可以使用 synchronize 或者是锁的方式来保证原子性。

  • 还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

指令重排

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

private static Map<String,String> value ;
private static volatile boolean flag = fasle ; //以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//1
flag = true ;//2
} //发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
} public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}

这里的 volatile 关键字主要是为了防止指令重排。

如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • singleton 对象指向分配的内存地址。(3)

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

总结

volatileJava 并发中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定义为 volatile 来用于保证内存可见性。

将这块理解透彻对我们编写并发程序时可以提供很大帮助。

号外

最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。

地址: https://github.com/crossoverJie/Java-Interview

你应该知道的 volatile 关键字的更多相关文章

  1. volatile 关键字笔记

    你应该知道的 volatile 关键字 当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据. ...

  2. Java单例你所不知道的事,与Volatile关键字有染

    版权声明:本文为博主原创文章,未经博主允许不得转载. 如果问一个码农最先接触到的设计模式是什么,单例设计模式一定最差也是“之一”. 单例,Singleton,保证内存中只有一份实例对象存在. 问:为什 ...

  3. 嵌入式程序员应知道的0x10个基本问题

     来源:网络 嵌入式程序员应知道的0x10个基本问题 1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)#define SECONDS_PER_YEAR (60 ...

  4. C#开发人员应该知道的13件事情

    本文讲述了C#开发人员应该了解到的13件事情,希望对C#开发人员有所帮助. 1. 开发过程 开发过程是错误和缺陷开始的地方.使用工具可以帮助你在发布之后,解决掉一些问题. 编码标准 遵照编码标准可以编 ...

  5. jvm运行机制和volatile关键字详解

    参考https://www.cnblogs.com/dolphin0520/p/3920373.html JVM启动流程 1.java虚拟机启动的命令是通过java +xxx(类名,这个类中要有mai ...

  6. 嵌入式程序员应知道的0x10个C语言Tips

    [1].[代码] [C/C++]代码 跳至 [1] ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ...

  7. 关于C#你应该知道的2000件事

    原文 关于C#你应该知道的2000件事 下面列出了迄今为止你应该了解的关于C#博客的2000件事的所有帖子. 帖子总数= 1,219 大会 #11 -检查IL使用程序Ildasm.exe d #179 ...

  8. 关于Java高并发编程你需要知道的“升段攻略”

    关于Java高并发编程你需要知道的"升段攻略" 基础 Thread对象调用start()方法包含的步骤 通过jvm告诉操作系统创建Thread 操作系统开辟内存并使用Windows ...

  9. 理工科应该的知道的C/C++数学计算库(转)

    理工科应该的知道的C/C++数学计算库(转) 作为理工科学生,想必有限元分析.数值计算.三维建模.信号处理.性能分析.仿真分析...这些或多或少与我们常用的软件息息相关,假如有一天你只需要这些大型软件 ...

随机推荐

  1. sql server DbHelperSQL类

    using JKTAC_LMIS.Entity; using System; using System.Collections; using System.Collections.Generic; u ...

  2. 业务线B/C端业务组件总结

    /** * 业务线组件总结 * */ /* B端组件的总结 1.组件cssBase的总结 1像素底部边框 */ @mixin border - 1px - b($background: $gray - ...

  3. Capslock+ 键盘党都爱的高效利器 - 让 Windows 快捷键操作更加灵活强大

    Capslock+ 键盘党都爱的高效利器 - 让 Windows 快捷键操作更加灵活强大  优化辅助    Windows   2016-06-05 91,167   微博微信QQ空间印象有道邮件   ...

  4. Lightscape

    Lightscape是一种先进的光照模拟和可视化设计系统,用于对三维模型进行精确的光照模拟和灵活方便的可视化设计. Lightscape是一个光照渲染软件,它特有的光能传递计算方式和材质属性所产生的独 ...

  5. dnmp(docker的lnmp)安装WordPress之后图片上传问题 问题:图片上传大小问题解决和 报错413 Request Entity Too Large

    首先是提示超过图片尺寸和大小, 最后发现都是图片大小的问题, 需要修改php的最大上传size 修改之后查看php配置  已经生效  但是还是报错, 提示返回不是合法的json,  查看控制台, 报错 ...

  6. 用clumsy模拟丢包测试socket库的失败重传

    用python的socket库写了通信小程序,现在我需要通过软件模拟出在网络极差的情况下,socket底层解决丢包问题的能力怎么样,我一开始想的是分别在linux和windowns下分别测试,后来一想 ...

  7. webServices 使用GET请求接口方法

    webServices  若要使用GET请求接口方法在Web.config 下添加这段 <webServices>     <protocols>       <add  ...

  8. springboot增删改查

    改https://blog.csdn.net/weixin_42338186/article/details/81561592 添加https://blog.csdn.net/weixin_42338 ...

  9. wpa_cli 关联无线网络

    fq关联无线网络,不同的无线网络认证方式不同设置:    1)open(开放式认证方式,分为):         wpa_cli -iwlan0 set_network 0 ssid '"w ...

  10. lnmp环境一些基本命令行

    使用service启动/停止/重启相关服务 启动/停止/重启 php服务 service php-fpm start/stop/restart 启动/停止/重启 mysql service mysql ...