面试题答案
一键面试共享变量可见性问题的原因
- 缓存一致性问题:现代CPU为了提高性能,每个CPU核心都有自己的高速缓存。当一个线程修改了共享变量的值,这个修改首先会写入到该线程所在CPU核心的高速缓存中。其他线程如果从自己的高速缓存中读取该变量,就可能读取到旧值,导致可见性问题。
- 指令重排序:Java编译器和CPU为了优化程序性能,会对指令进行重排序。在单线程环境下,重排序不会影响程序的最终执行结果。但在多线程环境中,重排序可能会导致一个线程对共享变量的修改,在另一个线程看来顺序是错误的,进而引发可见性问题。
使用volatile关键字和Java内存屏障解决可见性问题
- volatile关键字:
- 原理:当一个变量被声明为volatile时,对这个变量的读写会有特殊的内存语义。写入volatile变量时,会立即把该变量值从线程的工作内存刷新到主内存中。读取volatile变量时,会直接从主内存中读取最新值,而不是从线程的工作内存中读取,从而保证了不同线程对该变量的可见性。
- 结合内存屏障:Java内存模型使用内存屏障来实现volatile的内存语义。在对volatile变量进行写操作时,会在写操作后插入一个StoreStore屏障和一个StoreLoad屏障。StoreStore屏障保证在volatile写之前,所有的普通写操作都已经刷新到主内存;StoreLoad屏障防止volatile写与后续读/写操作重排序。在对volatile变量进行读操作时,会在读操作前插入一个LoadLoad屏障和一个LoadStore屏障。LoadLoad屏障保证在volatile读之后,所有的普通读操作都不会被重排序到volatile读之前;LoadStore屏障防止volatile读与后续写操作重排序。
- Java内存屏障:内存屏障是一组CPU指令,用于控制特定条件下的重排序和内存可见性问题。除了上述与volatile结合的内存屏障外,不同类型的内存屏障(如LoadLoad、LoadStore、StoreLoad、StoreStore)在不同场景下确保了内存操作的顺序性和可见性,保证多线程环境下数据的一致性。
volatile不能保证原子性的原因
- 原子性概念:原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。
- volatile不保证原子性的原因:
- 本质:volatile关键字主要解决的是可见性问题,它通过内存屏障等机制保证了对变量的修改能及时被其他线程看到。但是,对于像
i++
这样的复合操作(读取 - 修改 - 写入),它不是原子的。虽然volatile保证了读取和写入的可见性,但当多个线程同时执行i++
操作时,可能会出现以下情况:线程A读取i
的值,然后线程B也读取i
的值(此时两者读取的值相同),接着线程A对i
进行加1操作并写入主内存,线程B也对读取的值进行加1操作并写入主内存,这样最终i
只增加了1,而不是2,导致结果错误。因为volatile没有对整个复合操作进行同步控制,所以不能保证原子性。要保证原子性,需要使用synchronized
关键字或者java.util.concurrent.atomic
包下的原子类(如AtomicInteger
)。
- 本质:volatile关键字主要解决的是可见性问题,它通过内存屏障等机制保证了对变量的修改能及时被其他线程看到。但是,对于像