前言

不管是在面试还是实际开发中 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() +"执行完毕");
} public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start(); System.out.println("main 线程正在运行") ; Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
String value = sc.next();
if(value.equals("1")){ new Thread(new Runnable() {
@Override
public void run() {
aVolatile.stopThread();
}
}).start(); break ;
}
} System.out.println("主线程退出了!"); } 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 ;//
int b=20 ;//
int c= a+b ;//

一段特别简单的代码,理想情况下它的执行顺序是: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() ;//
flag = true ;//
} //发生在线程 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 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

总结

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

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

参考:https://crossoverjie.top/2018/03/09/volatile/


-END-

volatile 关键字了解与使用的更多相关文章

  1. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  2. volatile关键字 学习记录2

    public class VolatileTest2 implements Runnable{ volatile int resource = 0; public static void main(S ...

  3. volatile关键字 学习记录1

    虽然已经工作了半年了...虽然一直是在做web开发....但是平时一直很少使用多线程..... 然后最近一直在看相关知识..所以就有了这篇文章 用例子来说明问题吧 public class Volat ...

  4. 【转】Java并发编程:volatile关键字解析

    转自:http://www.importnew.com/18126.html#comment-487304 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备 ...

  5. 架构师养成记--4.volatile关键字

    volatile修饰的变量可在多个线程间可见. 如下代码,在子线程运行期间主线程修改属性值并不对子线程产生影响,原因是子线程有自己独立的内存空间,其中有主内存中的变量副本. public class ...

  6. java中volatile关键字的含义

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  7. volatile关键字详解

    本文系转载,原文链接:http://www.cnblogs.com/Chase/archive/2010/07/05/1771700.html,如有侵权,请联系我:534624117@qq.com 引 ...

  8. volatile关键字并不能作为线程计数器

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  9. volatile关键字及编译器指令乱序总结

    本文简单介绍volatile关键字的使用,进而引出编译期间内存乱序的问题,并介绍了有效防止编译器内存乱序所带来的问题的解决方法,文中简单提了下CPU指令乱序的现象,但并没有深入讨论. 以下是我搭建的博 ...

  10. 也来说说C/C++里的volatile关键字

    去年年底的样子,何登成写了一篇关于C/C++ volatile关键字的深度剖析blog(C/C++ Volatile关键词深度剖析).全文深入分析了volatile关键字的三个特性.这里不想就已有内容 ...

随机推荐

  1. 100-days: eleven

    Title: Facebook's live streaming(网络直播) is criticized(批评) after mosque(清真寺) shooting(枪击). live adj.现场 ...

  2. vue 自动识别PC、移动端,并跳转到对应页面

    app.vuehead中添加 <!--自动识别PC.移动--> <script src="static/js/uaredirect.js" type=" ...

  3. MAC book 无法删除普通用户的解决办法

    1来自苹果官网 macOS Sierra: 删除用户或群组 如果您是管理员,当您不想再让某些用户访问 Mac 时,可以删除他们.您也可以删除不想要的群组. 删除用户时,您可以存储该用户的个人文件夹(包 ...

  4. [leetcode]2. Add Two Numbers两数相加

    You are given two non-empty linked lists representing two non-negative integers. The digits are stor ...

  5. vue组件之时间组件

    效果图 主要有两个注意点,前面时分,通过定时器,1秒钟取一次,只要数据变了立刻让他展示,当然也可以1分钟取一次,我看了下定时器和真正的时间 其实有一定的偏差的,大约要1分多才会改变,所以我用了1秒取一 ...

  6. 探索未知种族之osg类生物---渲染遍历之Renderer简介

    我们继续renderingTraversals()的探究.我们接着上一节的”阻塞渲染线程”后就要遍历所有摄像机的渲染器(Renderer),执行 Renderer::cull 场景筛选的操作.我们在r ...

  7. iOS高德地图SDK定位和搜索附近信息的具体使用

    1.显示地图.定位.显示当前位置. 导入你需要的功能的头文件,申明全局变量,代理方法等等.   初始化地图,在控制器即将显示额时候打开定位和跟踪用户,这里对参数不懂的话康忙进去都有注释.   对了.i ...

  8. Python3个人学习笔记--每天一点一滴成长!

    简单的while循环:输入一个数字,while获取该数字,并输出该数字. 例子:猜幸运数字是多少? lucky_num = int(input("number:")) a = 0 ...

  9. Chapter3_操作符_关系操作符

    java中的关系操作符有等于“=”,不等于“!=”,大于“>”,小于“<”,大于等于“≥”,小于等于“≤”等.这些都不再赘述了,有一个有意思的点是关于等价性的讨论. 在java中测试两个对 ...

  10. Alpha 冲刺 (5/10)

    队名 火箭少男100 组长博客 林燊大哥 作业博客 Alpha 冲鸭鸭鸭鸭! 成员冲刺阶段情况 林燊(组长) 过去两天完成了哪些任务 协调各成员之间的工作 协助前后端接口的开发 测试项目运行的服务器环 ...