前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万字长文说透 volatile 的原理和面试知识点!

万字长文说透 volatile 的原理和面试知识点!

作者头像
业余草
发布2020-02-12 16:09:06
7770
发布2020-02-12 16:09:06
举报
文章被收录于专栏:业余草业余草

最近看到一篇很好的 volatile 可见性原理总结,分享给大家!

volatile 是一种轻量且在有限的条件下线程安全技术,它保证修饰的变量的可见性和有序性,但非原子性。相对于 synchronize 高效,而常常跟 synchronize 配合使用。

先简单说一下 Java 内存模型。这里主要描述的线程,工作内存,主存的变量的读写关系:

  • 主存存放线程需要操作的变量,但线程并不直接操作主存。
  • 每个线程读取主存变量都是先拷贝一份到工作内存中,不同线程工作内存互不干扰。
  • 线程修改了工作内存后,再写回主存中。
  • 每次从主存读写的过程都需要经过原子性操作。

volatile 可见性的特殊性。

  1. 操作 use 之前必须先执行 read 和 load 操作。
  2. 操作 assign 之后必须执行 store 和 write 操作。

由特性性保证了 read、load 和 use 的操作连续性,assign、store 和 write 的操作连续性,从而达到工作内存读取前必须刷新主存最新值;工作内存写入后必须同步到主存中。读取的连续性和写入的连续性,看上去像线程直接操作了主存。

volatile 是非原子性的,即它不具备原子性。

  1. use 和 assign 这两个操作整体上不是一个连续性的原子操作。volatile 本身并不对数据运算处理维持原子性,强调的是读写及时影响主存。
  2. 非原子性操作举例:volatile 修饰 num,num++;num = num+1;这种就是非原子性操作。它是分步进行操作的:先主存读取 num 的值;再进行 num++ 运算;然后将 num 值写到主存。

volatile 的有序性,volatile 能够禁止指令重排。

指令重排是指:为了提高性能,编译器和和处理器通常会对指令进行指令重排序。

上图中的三个重排位置可以调换的,根据系统优化需要进行重排。遵循的原则是单线程重排后的执行结果要与顺序执行结果相同。

内存屏障指令:volatile 在指令之间插入内存屏障,保证按照特定顺序执行和某些变量的可见性。

volatile 就是通过内存屏障通知 cpu 和编译器不做指令重排优化来维持有序性。

与 synchronize 的串行控制的区别:

  1. synchronize 无禁止指令重排。
  2. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,获取对象锁,互斥排他性达到两个同步块串行执行。

volatile 的线程安全适用范围是有条件的。由于 volatile 的非原子性原因,所以它的线程安全是有条件的:

  1. 运算结果不依赖但前置,或者能保证自由一个单一线程修改变量值。
  2. 变量不需要与其他的状态变量共同参与不变的约束。

这两条件描述出自于《深入理解java虚拟机》。

最后做个总结:

  1. volatile 具有可见性和有序性,不能保证原子性。
  2. volatile 在特定情况下线程安全,比如自身不做非原子性运算。
  3. synchronize 通过获取对象锁,保证代码块串行执行,无禁止指令重排能力。
  4. DCL 单例操作需要 volatile 和 synchronize 保证线程安全。

看完上面的内容,你真的懂了 volatile 吗?下面我们在看几个 volatile 常见的面试题吧。

1、volatile 关键字在 Java 中有什么作用?

volatile 是一个特殊的修饰符,只有成员变量才能使用它。

在 Java 并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。

volatile 变量可以保证下一个读取操作会在前一个写操作之后发生。

2、面试官: 继续,说说你对 volatile 关键字的理解。

就我理解的而言,被 volatile 修饰的共享变量,就具有了以下两点特性:

  1. 保证了不同线程对该变量操作的内存可见性;
  2. 禁止指令重排序

3、面试官: 能不能详细说下什么是内存可见性,什么又是重排序呢?

Java 虚拟机规范试图定义一种 Java 内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台上都能达到一致的内存访问效果。简单来说,由于 CPU 执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存。在 Java 内存模型里,对上述的优化又进行了一波抽象。JMM 规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成 CPU 上的寄存器或者高速缓存。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。

在线程执行时,首先会从主存中 read 变量值,再 load 到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。使用工作内存和主存,虽然加快的速度,但是也带来了一些问题。比如下面这个例子:

假设 i 初值为 0,当只有一个线程执行它时,结果肯定得到 1,当两个线程执行时,会得到结果 2 吗?这倒不一定了。可能存在这种情况:

如果两个线程按照上面的执行流程,那么i最后的值居然是 1 了。如果最后的写回生效的慢,你再读取 i 的值,都可能是 0,这就是缓存不一致问题。下面就要提到你刚才问到的问题了,JMM 主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的,通过解决这三个问题,可以解除缓存不一致的问题。而 volatile 跟可见性和有序性都有关。

4、面试官:你知道 volatile 底层的实现机制吗?

如果把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个 lock 前缀指令。lock 前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置
  2. 使得本 CPU 的 Cache 写入内存
  3. 写入动作也会引起别的 CPU 或者别的内核无效化其 Cache,相当于让新写入的值对别的线程可见。

5、面试官:volatile 的使用场景,请你举两个例子?

  1. 状态量标记,标记为 volatile 可以保证修改对线程立刻可见。比 synchronized,Lock 使用更方便,效率更好。
  2. 单例模式的实现,典型的双重检查锁定(DCL)。

6、volatile 变量和 atomic 变量有什么不同?

首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。

Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。

而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如 getAndIncrement() 方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

参考资料:

  • https://zh.wikipedia.org/zh-hans/Volatile%E5%8F%98%E9%87%8F
  • https://baike.baidu.com/item/volatile
  • https://juejin.im/post/5e17a5735188254c0a0409f1
  • https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-01-12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档