基本功
我是 javapub,一名 Markdown
程序员从👨💻,八股文种子选手。
面试官: 你能解释一下 volatile
关键字的作用吗?
候选人: 当我们在编写多线程程序时,经常会遇到线程安全的问题。其中一个常见的问题是可见性问题,即一个线程修改了共享变量的值,但是其他线程并不能立即看到这个修改。这时候,我们可以使用 volatile
关键字来解决这个问题。
面试官: 非常好。那么,你能具体说明一下 volatile
关键字是如何保证可见性的吗?
候选人: 当一个变量被声明为 volatile
后,每次访问这个变量时,都会从内存中读取最新的值,而不是使用 CPU 缓存中的旧值。同样地,每次修改这个变量时,都会立即将新值写入内存,而不是等到线程结束或者 CPU 缓存刷新时才写入。这样,其他线程就可以立即看到这个变量的最新值,从而保证了可见性。
在 JVM 中,volatile
关键字的实现涉及到以下几个方面:
- 内存屏障: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");
}
- 内存语义: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;
}
- 编译器优化: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追更!
🎁目录合集:
Gitee:https://gitee.com/rodert/JavaPub
GitHub:https://github.com/Rodert/JavaPub
http://javapub.net.cn