面试题答案
一键面试1. volatile关键字概述
在Java中,volatile
关键字用于修饰变量,它主要有两个作用:保证变量的可见性和禁止指令重排。
2. 可见性原理
- Java内存模型(JMM)基础:JMM定义了主内存和线程的工作内存。主内存是所有线程共享的,而每个线程都有自己的工作内存,线程对变量的操作都在工作内存中进行,然后再同步回主内存。
- 可见性问题:当一个线程修改了共享变量的值,其他线程可能无法立即看到这个修改。例如,线程A修改了变量
x
,但是线程B的工作内存中x
的副本还是旧值,直到线程B重新从主内存读取x
的值。 - volatile的解决方式:被
volatile
修饰的变量,当一个线程修改了这个变量的值,会立即将修改后的值刷新到主内存。而其他线程在使用这个变量前,会先从主内存中重新读取最新的值。这就保证了所有线程看到的volatile
变量的值是一致的,即保证了可见性。
3. 指令重排与内存屏障
- 指令重排:为了提高程序的执行效率,编译器和处理器会对指令进行重排序。在单线程环境下,指令重排不会影响程序的最终执行结果,但在多线程环境下,可能会导致程序出现错误。例如:
// 线程1
int a = 1; // 语句1
int b = 2; // 语句2
// 线程2
int sum = a + b; // 语句3
在单线程中,语句1和语句2可能会被重排,但不影响最终结果。但在多线程环境下,如果线程2在语句1执行前执行语句3,就会得到错误的结果。
- 内存屏障:
- 内存屏障分类:在JMM中,内存屏障分为LoadLoad、LoadStore、StoreStore、StoreLoad四种类型。
- volatile与内存屏障:对于
volatile
变量的写操作,会在写操作之后插入一个StoreStore屏障和一个StoreLoad屏障。StoreStore屏障保证在volatile
写操作之前的所有写操作都已经刷新到主内存;StoreLoad屏障保证volatile
写操作对其他线程可见,同时禁止后面的读操作重排到volatile
写操作之前。对于volatile
变量的读操作,会在读操作之前插入一个LoadLoad屏障和一个LoadStore屏障。LoadLoad屏障禁止volatile
读操作和前面的读操作重排;LoadStore屏障禁止volatile
读操作和后面的写操作重排。
4. 不同硬件架构下volatile实现的差异
- x86架构:在x86架构下,处理器使用缓存一致性协议(如MESI协议)来保证缓存的一致性。对于
volatile
变量的写操作,通常会使用mfence
指令,它会保证在该指令之前的所有写操作都已经完成并对其他处理器可见,同时禁止后续指令重排到mfence
指令之前。对于volatile
变量的读操作,x86架构下一般不需要额外的指令,因为其本身的缓存一致性协议已经能保证读操作的可见性。 - ARM架构:ARM架构下,
volatile
变量的实现依赖于特定的内存屏障指令。例如,dmb
(数据内存屏障)指令可以保证在该指令之前的所有内存访问操作都已经完成并对其他处理器可见,dsb
(数据同步屏障)指令则更严格,不仅保证内存访问操作完成,还会等待所有的缓存操作完成。在ARM架构下,对volatile
变量的读写操作可能需要更多的指令来保证可见性和禁止指令重排。
综上所述,volatile
关键字通过内存屏障机制在不同硬件架构下实现了可见性和禁止指令重排,从而保证了多线程环境下共享变量的正确访问。