《面试1v1》volatile

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

基本功

我是 javapub一名 Markdown 程序员从‍八股文种子选手。

面试官 你能解释一下 volatile 关键字的作用吗

候选人 当我们在编写多线程程序时经常会遇到线程安全的问题。其中一个常见的问题是可见性问题即一个线程修改了共享变量的值但是其他线程并不能立即看到这个修改。这时候我们可以使用 volatile 关键字来解决这个问题。

面试官 非常好。那么你能具体说明一下 volatile 关键字是如何保证可见性的吗

候选人 当一个变量被声明为 volatile 后每次访问这个变量时都会从内存中读取最新的值而不是使用 CPU 缓存中的旧值。同样地每次修改这个变量时都会立即将新值写入内存而不是等到线程结束或者 CPU 缓存刷新时才写入。这样其他线程就可以立即看到这个变量的最新值从而保证了可见性。

在 JVM 中volatile 关键字的实现涉及到以下几个方面

  1. 内存屏障JVM 会在 volatile 变量的读写操作前后插入内存屏障以保证指令不会被重排序。内存屏障可以分为读屏障、写屏障和全屏障分别用于保证读操作、写操作和所有操作的有序性。下面是 HotSpot JVM 中的 volatile 内存屏障实现
inline void OrderAccess::fence() {
  __asm__ volatile ("" : : : "memory");
}

inline void OrderAccess::loadload() {
  __asm__ volatile ("lfence" : : : "memory");
}

inline void OrderAccess::storestore() {
  __asm__ volatile ("sfence" : : : "memory");
}

inline void OrderAccess::loadstore() {
  __asm__ volatile ("mfence" : : : "memory");
}

inline void OrderAccess::storeload() {
  __asm__ volatile ("mfence" : : : "memory");
}
  1. 内存语义JVM 的内存模型规定了共享变量的访问方式以及如何保证可见性和有序性。对于 volatile 变量JVM 会保证每次读取都从内存中读取最新的值每次写入都立即写入内存以保证可见性和有序性。下面是 HotSpot JVM 中的 volatile 内存语义实现
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest)
                    , "m" (*dest)
                    : "cc", "memory");
  return exchange_value;
}

inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchg8b (%3)"
                    : "=A" (exchange_value)
                    : "b" ((jint)exchange_value), "c" ((jint)(exchange_value >> 32)), "r" (dest)
                    , "m" (*dest)
                    : "cc", "memory");
  return exchange_value;
}
  1. 编译器优化JVM 的编译器会对代码进行优化以提高程序的性能。但是对于 volatile 变量编译器会禁止一些优化以保证指令不会被重排序。比如编译器不会将 volatile 变量的读写操作与其他指令重排序也不会将 volatile 变量的读操作和写操作合并为一个操作。下面是 HotSpot JVM 中的 volatile 变量读写操作的实现
inline jint    Atomic::load    (volatile jint*    p) { return *p; }
inline jlong   Atomic::load    (volatile jlong*   p) { return *p; }
inline jfloat  Atomic::load    (volatile jfloat*  p) { return *p; }
inline jdouble Atomic::load    (volatile jdouble* p) { return *p; }

inline void    Atomic::store   (volatile jint*    p, jint    x) { *p = x; }
inline void    Atomic::store   (volatile jlong*   p, jlong   x) { *p = x; }
inline void    Atomic::store   (volatile jfloat*  p, jfloat  x) { *p = x; }
inline void    Atomic::store   (volatile jdouble* p, jdouble x) { *p = x; }

面试官 很好。那么你能否举一个例子来说明 volatile 关键字的作用呢

候选人 当然。比如我们可以定义一个 flag 变量并在一个线程中修改它的值然后在另一个线程中读取它的值。如果 flag 变量没有被声明为 volatile那么在另一个线程中读取 flag 变量的值时可能会看到旧值而不是最新的值。但是如果 flag 变量被声明为 volatile那么在另一个线程中读取 flag 变量的值时就可以保证看到最新的值。

下面是一个简单的示例代码演示了 volatile 关键字的作用

public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public void doSomething() {
        while (!flag) {
            // do something
        }
        // do something else
    }
}

在这个示例中我们定义了一个 VolatileExample 类其中包含一个 flag 变量。在 doSomething() 方法中我们使用了一个 while 循环来等待 flag 变量的值变为 true。如果 flag 变量没有被声明为 volatile那么在另一个线程中调用 setFlag(true) 方法后doSomething() 方法可能会一直等待下去因为它看不到 flag 变量的修改。但是由于 flag 变量被声明为 volatile所以在另一个线程中调用 setFlag(true) 方法后doSomething() 方法会立即看到 flag 变量的修改从而退出循环。

面试官 非常好。那么你认为 volatile 关键字有什么缺点吗

候选人 volatile 关键字只能保证可见性不能保证原子性。如果一个变量的修改涉及到多个步骤那么使用 volatile 关键字可能会导致线程安全问题。在这种情况下我们需要使用其他的同步机制比如 synchronized 关键字或者 Lock 接口。

面试官 很好。你对 volatile 关键字的理解非常清晰。部分是比较考验工程师基本功的你回答的很好这部分可以过了。

候选人 非常感谢。

最近我在更新《面试1v1》系列文章主要以场景化的方式讲解我们在面试中遇到的问题致力于让每一位工程师拿到自己心仪的offer感兴趣可以关注JavaPub追更

目录合集

Giteehttps://gitee.com/rodert/JavaPub

GitHubhttps://github.com/Rodert/JavaPub

http://javapub.net.cn

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6