面试官:你平时是怎么创建单例的?

我:我一般用DCL双重检锁的方式来创建单例,然后为 instance 加上 volatile 修饰,防止 DCL 失效。

面试官:那你可以具体说说 volatile 吗?

我:行!

前言

相信很多 Andorid程序员跟我一样,最开始接触到 volatile 这个关键字是在创建单例的时候,如:

public class SingleTon {//为了防止出现 DCL失效问题,加上 volatile 关键字private static volatile SingleTon instance;public static SingleTon getInstance() {if (instance == null) {//同步锁,保证同一时刻只有一个线程进入该代码块。synchronized (SingleTon.class) {if (instance == null) {instance = new SingleTon();}}}return instance;}}

当我们使用双重检锁(DCL)来创建单例的时候,我们会为 instance 加上 volatile关键字修饰,来防止出现DCL失效。这里其实就是利用 volatile 可以禁止指令重排序功能,来防止出现 DCL 失效问题。

那 volatile 到底是如何防止出现DCL失效,是怎么做到的呢?让我们一起来往下学习。

另外如果你想进一步了解 Synchronized,可以看我的另一篇文章 Android程序员重头学Synchronized


线程安全

就如刚刚那个创建单例的代码来说,我们需要考虑其在多线程下的运行情况,也就是在多线程并发中,线程是否安全?那线程在什么样的情况下我们可以称之为是线程安全呢?

答: 线程在保证 可见性、有序性、原子性 的情况下,就可以称之为是线程安全的。

那可见性、有序性、原子性又是什么呢?别急~,在介绍这三种特性之前,我们需要先了解一下 Java内存模型。

Java内存模型(Java Memory Model,JMM): 是 Java虚拟机规范中定义的,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java程序在各种平台下都能达到一致的并发效果,JMM 规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

嗯~~??你还是去一下看 深入理解Java虚拟机 12.3 Java内存模型 吧。

可见性

我们都知道在计算机中 CPU 的计算速度是非常快的,但是绝大多数的计算任务光靠 CPU 是完不成的,CPU 需要与内存进行交互,也就是从内存中获取数据及将计算结果写入到内存中,相对比,这个速度就很慢了。因此,间接导致了 CPU 的计算速度大打折扣,所以为了解决这个问题,现在 CPU 厂商会在 CPU 中内置高速缓冲存储器,CPU 不再直接与内存进行交互,而是与高速缓冲存储器进行信息交换。

之前看到的一个段子,很贴切了:

内存:你跑慢点行不行?

CPU:跑慢点你养我吗?

内存:我不管!

CPU:那我只能找高速缓冲存储器了!

那这高速缓冲存储器是什么呢?百度百科是这么介绍的:

高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储芯片(SRAM)组成,容量比较小但速度比主存高得多, 接近于CPU的速度

主要由三大部分组成:

  • Cache存储体:存放由主存调入的指令与数据块。
  • 地址转换部件:建立目录表以实现主存地址到缓存地址的转换。
  • 替换部件:在缓存已满时按一定策略进行数据块替换,并修改地址转换部件。

从此,CPU 直接从高速缓冲存储器中读取数据,计算速度大大提升,但这也存在了一个问题,那就是高速缓冲存储器与主内存数据的同步问题,准确的说是在多核多线程条件下就会发生高速缓冲存储器中的数据内容与内存中的数据内容不一致问题。

如上图所示,线程1 持有的是高速缓存1,线程2 持有的高速缓存2,高速缓存1 与 高速缓存2 中的数据都是从同一个内存中读取的。一开始,两个线程的数据内容肯定是一样的,但是两个线程都可以更改自己持有的高速缓存中的数据内容,并且两者互不干扰,这也就导致了数据不一致问题

比如:一开始,两个线程都从内容中读取了 num = 1, 但是过了一会,线程2 将 num 改为了 2,然后写入到内存中,这时,线程1 没有再去内存中拿新的 num = 2 新值,而是直接用 num = 1 这个旧值,这就存在问题了。

所以为了保证可见性,当一个线程更新了内存中的共享变量时,需要通知其他线程重新从内存中读取值

关于内存与工作内存,在 《深入理解Java虚拟机 12.3Java内存模型》 中是这么介绍的:

每条线程还有自己的工作内存(可与处理器的高速缓存类比),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。

ok,了解了可见性,接着我们再来看看有序性。

有序性

看一段代码

int age = 10;
boolean isAdult = false;
//修改数值
age = 20; //修改年龄
isAdult = true; //修改是否成年

针对上述代码,你觉得是会先修改年龄再修改是否成年呢?还是先修改是否成年然后再修改年龄呢?

答案是:都不一定!

按照代码顺序,肯定是会先执行 age = 20;然后再执行isAdult = true;,但其实JVM会考虑性能效率问题然后对指令进行重排序,所以答案是不一定。

为了提高性能,编译器和处理器可能会对指令做重排序,重排序可以分为三种:

  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 补充数据依赖性:如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性,这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,这三种操作都是存在数据依赖性的,这时候如果进行重排序就会对最终执行结果产生影响。所以编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

原子性

原子性(Atomicity)就是指对数据的操作是一个独立的、不可分割的整体,即:一个操作或多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。

JMM 来直接保证的原子性变量操作有 read、load、use、assign、store、write 这六个。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入到主内存的变量中。

所以大致认为基本数据类型访问、读写都是具备原子性的。但如果需要一个更大范围的原子性保证,JMM 还提供了lock 和 unlock 操作来完成。

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM 并没有把 lock 和 unlock 直接开放出来供用户使用,但是提供了 monitorenter 与 monitorexit 两个直接码来隐式使用这两个操作。如果你不了解 monitorenter 与 monitorexit 这两个指令,可以看我的另一篇文章 Android程序员重头学Synchronized。


实现原理

volatile如何防止DCL失效

现在,我们回归一开始的 DCL失效问题。

public class SingleTon {//为了防止出现 DCL失效问题,加上 volatile 关键字private static volatile SingleTon instance;public static SingleTon getInstance() {if (instance == null) {//同步锁,保证同一时刻只有一个线程进入该代码块。synchronized (SingleTon.class) {if (instance == null) {instance = new SingleTon();}}}return instance;}}

其实 DCL失效的本质,正是由于指令重排序导致的,在具体一点就是上述代码中的instance = new SingleTon();这条实例化 SingleTon 对象代码,因为它不是一个原子操作。

在JVM中,实例化一个对象分为三个步骤:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 指向刚分配的内存空间地址

但由于 JVM 会对指令进行重排序,所以上面的步骤2与步骤3顺序可能发生改变,可能会变成:

  1. 分配对象的内存空间
  2. 指向刚分配的内存空间地址
  3. 初始化对象

所以在多线程的条件下,刚刚的 DCL 代码可能会出现这样的情况:
线程1获取锁,进入同步代码块,这时instance == null,所以执行instance = new SingleTon();,给 instance 分配内存空间,然后指向刚刚分配的空间地址,这时候也就意味着 instance 不为 null 了,然后刚准备执行 SingleTon() 构造方法来初始化时,这时线程2调用了getInstance()方法,此时instance != null,所以会直接返回一个不为 null 但是未完成初始化的 instance 对象

所以我们为 instance 加上 volatile 关键字修饰,来禁止指令重排序,就可以避免这个问题出现。

那 volatile 是怎么做到的呢?

volatile是如何做到禁止指令重排序的

volatile 其实通过内存屏障(Memory Barrier)来防止指令重排序的,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

屏障类型 指令示例 说明
LoadLoad屏障 Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore屏障 Store1;StoreStore;Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore屏障 Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad屏障 Store1;StoreLoad;Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令
  • Load:将内存存储的数据拷贝到处理器的高速缓存中。
  • Store:将处理器高速缓存中的数据刷新到内存中。

为 volatile写操作的前后都插入StoreStore屏障操作,保证写操作按顺序将缓存中的数据写入内存中。

为 volatile读操作的后面分别插入LoadLoad屏障LoadStore屏障,保证读操作按顺序将内存中的数据复制到缓存中。

volatile是如何做到及时可见性的

当共享变量被 volatile 修饰后,在多线程环境下,当一个线程对它进行修改值后,会立即写入到内存中,然后让其他所有持有该共享变量的线程的工作内存中的值过期,这样其他线程就必须去内存中重新获取最新的值,从而做到共享变量及时可见性。

volatile 不能保证原子性

volatile 可以保证可见性,禁止指令重排序,而且又说它比 Synchronized 轻量,那这样的话,我们是不是直接用 volatile 就行了呀,还用啥 Synchronized。

别急,让我们来看段代码:

public class VolatileIncreaseTest {public static volatile int count = 0;public static void increase() {count++;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {increase();}System.out.println(Thread.currentThread().getName() + " race = " + count);}}).start();}}}

其执行结果为:

Thread-5 count = 26031
Thread-7 count = 24936
Thread-3 count = 20780
Thread-9 count = 25867
Thread-6 count = 25720
Thread-1 count = 18145
Thread-2 count = 19876
Thread-4 count = 22125
Thread-8 count = 25488
Thread-0 count = 18527

咦~ ,我不是为 count 变量加上了 volatile 关键字修饰了吗?他不是可以在多线程并发环境下保证及时可见性吗?怎么 count 最终的输出结果没有增加到 100000 呢?

你可以自己试着去写一遍这个代码,其实 IDE 会给你提示:

提示我们说:我们对 volatile 修饰的 count 进行非原子性操作。

那如何才能保证原子性呢?

可以用 Synchronized 关键字(如果你想进一步了解 Synchronized,可以看我的另一篇博客 Android程序员重头学Synchronized)

针对上述代码,我们为 increase() 方法加上 Synchronized 关键字,如:

public static synchronized void increase() {count++;
}

你可以发现 IDE 的警告消失了,再次执行一下,其结果如下:

Thread-1 count = 89753
Thread-8 count = 100000
Thread-3 count = 98947
Thread-9 count = 97470
Thread-6 count = 88132
Thread-0 count = 71229
Thread-4 count = 96263
Thread-2 count = 91294
Thread-5 count = 98571
Thread-7 count = 99592

根据结果可以发现 count 最终会增加到 10000,达到了预期效果。

所以volatile不能保证原子性


总结

volatile关键字用于修饰变量,保证该变量在某一线程中数值发生更新时,其他持有该共享变量的线程可以及时知道,从而及时更新到最新的数值,即保证线程及时可见性,volatile 还可以禁止指令重排序,从而保证有序性,但是 volatile 不能保证原子性,所以不能保证线程安全,但是如果 volatile 修饰的变量的所有的操作都是原子性的(比如修饰一个 flag 变量,flag 只有赋值操作,赋值操作是原子性的),那么也是可以保证是线程安全的。


参考文献:
深入理解Java虚拟机
一文解决内存屏障


OK, 到这文章也就结束了。

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。 另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!

Android Volatile 关键字学习相关推荐

  1. java中volatile关键字---学习笔记

    volatile关键字的作用 在java内存模型中,线程之间共享堆内存(对应主内存),但又各自拥有自己的本地内存--栈内存,线程的栈内存中缓存有共享变量的副本,但如果是被volatile修饰的变量,线 ...

  2. Android Synchronized 关键字学习

    面试官:能说说 Synchronized 吗? 答:Synchronized 是Java的一个关键字,使用于多线程并发环境下,可以用来修饰实例对象和类对象,确保在同一时刻只有一个线程可以访问被Sync ...

  3. 重点知识学习(8.2)--[JMM(Java内存模型),并发编程的可见性\原子性\有序性,volatile 关键字,保持原子性,CAS思想]

    文章目录 1.JMM(Java Memory Model) 2.并发编程的可见性 3.并发编程的有序性 4.并发编程的原子性 5.volatile 关键字 6.保持原子性: 加锁,JUC原子类 加锁 ...

  4. volatile关键字在Android中到底有什么用?

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新. 上周六在公众号分享了一篇关于Java volatile关键字的文章,发布之后有朋友在留言里指 ...

  5. C语言学习及应用笔记之四:C语言volatile关键字及其使用

    在C语言中,还有一个并不经常使用但却非常有用的关键字volatile.那么使用volatile关键字究竟能干什么呢?接下来我将就此问题进行讨论. 一个使用volatile关键字定义变量,其实就是告诉编 ...

  6. 爆赞,对 volatile 关键字讲解最好的一篇文章!

    欢迎关注方志朋的博客,回复"666"获面试宝典 最近,在一篇文章中了解到了 volatile 关键字,在强烈的求知欲趋使下,我查阅了一些相关资料进行了学习,并将学习笔记记录如下,希 ...

  7. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

    前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...

  8. android volatile的使用

    今天,简单讲讲android里的volatile的使用. 这个其实很简单,而且我基本没有用到,但是还是记录一下.volatile的作用基本和sychronized相似,但是不能替代sychronize ...

  9. java多线程编程核心技术 pdf_Java多线程编程核心技术之volatile关键字

    私信我或关注公众号猿来如此呀,回复:学习,获取免费学习资源包 volatile关键字 关键字volatile的主要作用是使变量在多个线程间可见. 1 关键字volatile与死循环 如果不是在多继承的 ...

最新文章

  1. 稀疏多项式的运算用链表_用漫画告诉你—什么是HashMap?
  2. 浅谈session,cookie,sessionStorage,localStorage的区别及应用场景
  3. cuba.platform_CUBA 7.2 –有什么新功能?
  4. java开心消消乐代码_Vue实现开心消消乐游戏算法
  5. 中值滤波时K = filter2(fspecial('average',3),img)/255,原因
  6. bat自动登录服务器取文件,批处理(.bat)一键备份资料,自动登录局域网进行备份,加~批处理.bat加密软件...
  7. python面试文件操作_python基础-三分钟搞定面试官爱问的【文件操作】
  8. matlab的v带优化设计,基于遗传算法及MATLAB的V带传动优化设计
  9. URI 、URL 和 URN
  10. linux查看硬件信息及驱动设备
  11. 牛顿三次插值 matlab,matlab 牛顿插值法 三次样条插值法[行业二类]
  12. h2o api java_h2o H2OAutoEncoderEstimator
  13. autojs的使用文档
  14. [云原生专题-34]:K8S - 核心概念 - 网络 - Web服务器与反向代理服务器nginx入门介绍
  15. ScyllaDB 1.2 国内安装更新源发布
  16. VS2017添加lib静态库文件引用
  17. jenkins--将构建结果上传到构建页面(Archive the artifacts)
  18. “宇宙时钟”脉冲星太空定位精度达5公里
  19. 编译kernel外部模块
  20. ASEMI代理Infineon英飞凌IPB60R099CP原厂MOS管

热门文章

  1. algol语言_在21世纪探索Algol 68
  2. 浅谈Green公式和外微分形式
  3. Weblogic 常见漏洞汇总
  4. 通过 Windows 用户模式回调实施的内核攻击
  5. sv中packed struct
  6. 【Source教程】文章目录
  7. qt添加窗口边框阴影
  8. linux下curses库介绍
  9. Android X5WebView网络监听替换WebView失败页面稳定
  10. 太阳直射点纬度计算公式_高中地理——每日讲1题(气压带与风带、地方时、太阳高度角)...