面试被问傻了,同事说不懂volatile关键字,由浅入深讲解volatile

发布时间:04-0420:32

前言

随着互联网企业的兴起,对我们技术的要求也越来越高,很多时候企业又想省钱,又想发挥出机器的最大性能,真是累坏了程序员们。

当然,想要适应社会的进步,程序员也要不断的给自己充电,但人能忘本,基础知识还是要学扎实的。

这不,有位同学就来找我诉苦了,前两次面试都挺顺利的,到了三面竟然栽在了volatile关键字上。

下面我们就来好好聊聊volatile

volatile

volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略

volatile 的特性

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)禁止进行指令重排序。(实现有序性)volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。关于volatile 原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步,就跟下面的SoWhat跟SynSoWhat功能类似哦。

class SoWhat{volatile int i = 0; // volatile修饰的变量public int getI(){return i;// 单个volatile变量的读}public void setI(int j){this.i = j; // 单个volatile 变量的写}public void inc(){i++;//复合多个volatile 变量}}class SynSoWhat{int i = 0;public synchronized int getI(){return i;}public synchronized void setI(int j){this.i = j;}public void inc(){ // 普通方法调用int tmp = getI(); // 调用已同步方法tmp = tmp + 1;//普通写方法setI(tmp);// 调用已同步方法}}

写理解

volatile写的内存语义如下:

当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。

public class VolaSemanteme {int a = 0; volatile boolean flag = false; // 这是重点哦 public void init() { a = 1; flag = true; //....... } public void use() { if (flag) { int i = a * a; } //....... }}

线程A调用init方法,线程B调用use方法。

读理解

volatile读的内存语义如下:

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

public class VolaSemanteme {int a = 0; volatile boolean flag = false; // 这是重点哦 public void init() { a = 1; flag = true; //....... } public void use() { if (flag) { int i = a * a; } //....... }}

流程图大致是这样的:

volatile 指令重排

volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。关于内存屏障的具体讲解以前写过不再重复,JMM装逼于无形这里说过。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:

第一个为读操作时,第二个任何操作不可重排序到第一个前面。第二个为写操作时,第一个任何操作不可重排序到第二个后面。第一个为写操作时,第二个的读写操作也不运行重排序。

volatile写底层实现

JMM对volatile的内存屏障插入策略

在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。

volatile 读底层

JMM对volatile的内存屏障插入策略

在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。

中重点说下volatile读后面为什么跟了个LoadLoad。加入我有如下代码 AB两个线程执行,B线程的flag获取下面的读被提前了。

volatile的实现原理

有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。在CPU级别的功能如下:

将当前处理器缓存行的数据写回到系统内存

这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。

我们可以通过jitwatch对简单的代码进行详细的反汇编看一下。

package com.sowhat.demo;public class VolaSemanteme {int unvloatileVal = 0;volatile boolean flag = false;public void init() {unvloatileVal = 1;flag = true; // 第九行哦}public void use() {if (flag) {int LocalA = unvloatileVal;if (LocalA == 0) {throw new RuntimeException("error");}}}public static void main(String[] args) {VolaSemanteme volaSemanteme = new VolaSemanteme();volaSemanteme.init();volaSemanteme.use();}}

对普通变量的赋值操作:

对volatile变量的赋值操作。

可以对比得出,volatile 修饰的变量确实会多一个 lock addl $0x0,(%rsp) 指令。

0x0000000114ce95cb: lock addl $0x0,(%rsp) ;*putfield flag; - com.sowhat.demo.VolaSemanteme::init@7 (line 9)

使用 volatile 必须具备的条件

对变量的写操作不依赖于当前值。该变量没有包含在具有其他变量的不变式中。只有在状态真正独立于程序内其他内容时才能使用 volatile。

状态标志

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;......public void shutdown() { shutdownRequested = true; }public void doWork() {while (!shutdownRequested) {// do stuff}}

一次性安全发布(one-time safe publication)

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

public class BackgroundFloobleLoader {public volatile Flooble theFlooble;public void initInBackground() {// do lots of stufftheFlooble = new Flooble(); // this is the only write to theFlooble}}public class SomeOtherClass {public void doWork() {while (true) {// do some stuff...// use the Flooble, but only if it is readyif (floobleLoader.theFlooble != null)doSomething(floobleLoader.theFlooble);}}}

模独立观察(independent observation)

安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

public class UserManager {public volatile String lastUser;public boolean authenticate(String user, String password) {boolean valid = passwordIsValid(user, password);if (valid) {User u = new User();activeUsers.add(u);lastUser = user;}return valid;}}

volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

@ThreadSafepublic class Person {private volatile String firstName;private volatile String lastName;private volatile int age;public String getFirstName() { return firstName; }public String getLastName() { return lastName; }public int getAge() { return age; }public void setFirstName(String firstName) {this.firstName = firstName;}public void setLastName(String lastName) {this.lastName = lastName;}public void setAge(int age) {this.age = age;}}

开销较低的读-写锁策略

volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafepublic class CheesyCounter {// Employs the cheap read-write lock trick// All mutative operations MUST be done with the 'this' lock held@GuardedBy("this") private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value++;}}

双重检查(double-checked)

单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。

class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {syschronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}}

推荐懒加载优雅写法 Initialization on Demand Holder(IODH)。public class Singleton {static class SingletonHolder {static Singleton instance = new Singleton();}public static Singleton getInstance(){return SingletonHolder.instance;}}

好了,今天就聊到这里,谢谢大家的阅读,喜欢可以关注我哦!

返回顶部