内存可见性

volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,倘若能恰当的合理的使用volatile,自然是美事一桩。

为了能比较清晰彻底的理解volatile,我们一步一步来分析。首先来看看如下代码:

public class TestVolatile {boolean status = false;/*** 状态切换为true*/public void changeStatus(){status = true;}/*** 若状态为true,则running。*/public void run(){if(status){System.out.println("running....");}}
}

上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,可以保证输出"running....."吗?

答案是NO! 

这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,先运行changeStatus方法,再执行run方法,自然是可以正确输出"running...."的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量status来说,线程A的修改,对于线程B来讲,是"不可见"的。也就是说,线程B此时可能无法观测到status已被修改为true。那么什么是可见性呢?

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。


Java内存模型

为什么出现这种情况呢,我们需要先了解一下JMM(java内存模型)

java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下:

需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存。当然如果是出于理解的目的,这样对应起来也无不可。

大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,比如我们上文中的status,线程A将其修改为true这个动作发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B缓存了status的初始值false,此时可能没有观测到status的值被修改了,所以就导致了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile

volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;

2.这个写会操作会导致其他线程中的缓存无效。

上面的例子只需将status声明为volatile,即可保证在线程A将其修改为true时,线程B可以立刻得知:

 volatile boolean status = false;

留意复合类操作

但是需要注意的是,我们一直在拿volatile和synchronized做对比,仅仅是因为这两个关键字在某些内存语义上有共通之处,volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile并不能胜任。看下这个例子:

package test;import java.util.concurrent.CountDownLatch;/*** Created by chengxiao on 2017/3/18.*/
public class Counter {public static volatile int num = 0;//使用CountDownLatch来等待计算线程执行完static CountDownLatch countDownLatch = new CountDownLatch(30);public static void main(String []args) throws InterruptedException {//开启30个线程进行累加操作for(int i=0;i<30;i++){new Thread(){public void run(){for(int j=0;j<10000;j++){num++;//自加操作}countDownLatch.countDown();}}.start();}//等待计算线程执行完countDownLatch.await();System.out.println(num);}
}

执行结果:

224291

针对这个示例,一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是300000么?

问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:

1.读取 —— 2.加一 —— 3.赋值

所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。

解决num++操作的原子性问题

针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的。

/*** Created by chengxiao on 2017/3/18.*/
public class Counter {  //使用原子操作类public static AtomicInteger num = new AtomicInteger(0);//使用CountDownLatch来等待计算线程执行完static CountDownLatch countDownLatch = new CountDownLatch(30);public static void main(String []args) throws InterruptedException {//开启30个线程进行累加操作for(int i=0;i<30;i++){new Thread(){public void run(){for(int j=0;j<10000;j++){num.incrementAndGet();//原子性的num++,通过循环CAS方式}countDownLatch.countDown();}}.start();}//等待计算线程执行完countDownLatch.await();System.out.println(num);}
}

执行结果:

300000

关于原子类操作的基本原理,会在后面的章节进行介绍,此处不再赘述。


禁止指令重排序

volatile还有一个特性:禁止指令重排序优化。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:

1.重排序操作不会对存在数据依赖关系的操作进行重排序。

比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a

public class TestVolatile {int a = 1;boolean status = false;/*** 状态切换为true*/public void changeStatus(){a = 2;//1status = true;//2}/*** 若状态为true,则running。*/public void run(){if(status){//3int b = a+1;//4System.out.println(b);}}
}

假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?

答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

volatile禁止指令重排序也有一些规则,简单列举一下:

1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序

2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序

3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序


总结:

简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。关于原子操作类,会在后续的文章进行介绍。

谈谈Java中的volatile相关推荐

  1. 谈谈JAVA中的安全发布

    谈谈JAVA中的安全发布 昨天看到一篇文章阐述技术类资料的"等级",看完之后很有共鸣.再加上最近在工作中越发觉得线程安全性的重要性和难以捉摸,又掏出了<Java并发编程实战& ...

  2. 面试:说说Java中的 volatile 关键词?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | Matrix海子 来源 | https://w ...

  3. 如何理解 JAVA 中的 volatile 关键字

    如何理解 JAVA 中的 volatile 关键字 最近在重新梳理多线程,同步相关的知识点.关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂. ...

  4. java中的Volatile关键字使用

    文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...

  5. Java中的Volatile如何工作? Java中的volatile关键字示例

    如何在Java中使用Volatile关键字 在Java采访中,什么是volatile变量以及何时在Java中使用volatile变量是Java 采访中一个著名的多线程采访问题 . 尽管许多程序员都知道 ...

  6. java中二进制怎么说_面试:说说Java中的 volatile 关键词?

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

  7. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  8. 谈谈java中成员变量与成员方法继承的问题

    谈谈java中成员变量与成员方法继承的问题 关于成员变量和成员方法的的继承问题,我也可以做一个小测试,来看看结果. 首先我们先创建一个父类: 其次再创建一个子类,子类中要比父类中少一个成员方法: 这样 ...

  9. Java中的volatile

    文章目录 1.volatile的内存语义 2.内存屏障 2.happens-before 之 volatile 变量规则 4.Demo 1.volatile的内存语义 内存可见性 ​ volatile ...

最新文章

  1. redis使用epoll
  2. 基于纯Java代码的Spring容器和Web容器零配置的思考和实现(3) - 使用配置
  3. 21天学通python电子版-小数据池,深浅拷贝,集合+菜中菜
  4. support library目录解释说明内容
  5. [转]有关IIS的虚拟目录的控制总结
  6. layui select下拉框改变之 change 监听事件
  7. 0中断优先级_西门子S7-200 SMART中断及中断指令概述
  8. (四)RabbitMQ消息队列-服务详细配置与日常监控管理
  9. C++ vector容器中用erase函数和迭代器删除重复元素问题分析
  10. Visual Studio 添加 自定义 路径宏
  11. matlab 函数 命名参数,如何处理MATLAB中的函数参数的名称/值对
  12. Java1.8接口方法都是抽象_抽象类和接口的区别以及jdk1.8之后接口里面可以实现方法...
  13. win10添加环境变量后没用_python 学习之在 win10 下安装 Anaconda
  14. JSK-372 图案输出【入门】
  15. javascript 经常会用到的东西
  16. (日常搬砖)windows下如何查看并导出文件夹目录
  17. 推荐CSDN排名前1000博主
  18. java textarea滚动条,textarea添加滚动条 textarea 如何设置滚动条
  19. 中国大陆省市区县三级、四级菜单数据整理
  20. chorme浏览器广告终结者视频白频解决办法

热门文章

  1. 18、计算机图形学——BRDF与渲染方程
  2. HALCON No finder pattern could be found错误解决
  3. GXGetImage方式连续采集和发送软触发采集
  4. 在此处打开命令窗口_这样操作方便多了!简单DOS命令实用技巧详解
  5. ASP.NET MVC中你必须知道的13个扩展点
  6. 发现了一个delphi的form的bug
  7. 最先进数据中心都建在哪?
  8. bash编程-循环控制的结构
  9. Android 自定义ProgressDialog
  10. 移动应用性能测试白皮书