面试官:说说 volatile 应用和实现原理?
volatile 是并发编程中的重要关键字,它的名气甚至是可以与 synchronized、ReentrantLock 等齐名,也是属于并发编程五杰之一。
需要注意的是 volatile 并不能保证原子性,因此使用 volatile 并没有办法保证线程安全。
并发编程五杰:
PS:“并发编程五杰”是我个人起的名字,大家也不用太当真。
1.什么是 volatile?
volatile 是 Java 中的一个关键字,用于修饰变量,它的主要作用是保证变量的可见性和禁止指令重排序。
可见性:是指当一个线程修改了一个被 volatile 修饰的变量时,其他线程能够立即看到这个修改。
禁止指令重排序:则是确保对 volatile 变量的读写操作不会被编译器或处理器随意重新排序,从而保证了程序执行的顺序符合我们的预期。
2.volatile 工作原理
为了实现可见性,Java 内存模型(JMM)会在对 volatile 变量进行写操作时,强制将工作内存中的值刷新到主内存,并在读取时强制从主内存中重新获取最新的值。
而禁止指令重排序是通过在编译器和处理器层面添加特定的内存屏障指令来实现的。
具体来说。
2.1 可见性实现原理
可见性:在计算机编程特别是多线程编程中,“可见性”指的是一个线程对共享变量的修改,对于其他线程是否能够及时地、准确地“可见”,即其他线程是否能够及时感知到这个修改并获取到最新的值。
例如,在一个多线程环境中,如果线程 A 修改了一个共享变量的值,而线程 B 无法立即看到这个修改,那么就存在可见性问题。
多线程操作共享变量流程如下:
volatile 是通过内存屏障(Memory Barrier) 来确保可见性。
写屏障(Store Barrier):在 volatile 变量的写操作之后插入写屏障,确保所有之前的写操作都同步到主内存中,从而使得其他线程在读取该变量时能够获取到最新的值。
读屏障(Load Barrier):在 volatile 变量的读操作之前插入读屏障,确保所有之前的写操作都已完成,从而读取到的是最新的值。
通过这种方式,volatile 变量在多线程环境下的读写操作能够保持较高的可见性,但需要注意的是,volatile 并不保证操作的原子性。
具体来说,volatile 内存可见性主要通过 lock 前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通知其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。
2.2 有序性实现原理
volatile 的有序性是通过插入内存屏障,在内存屏障前后禁止重排序优化,以此实现有序性的。
2.3 正确理解“内存屏障”?
volatile 保证可见性的“内存屏障”和保证有序性的“内存屏障”有什么区别呢?
在说它们的区别之前,我们现需要对“内存屏障”有一个大致的理解。
内存屏障,简单来说,就像是在内存操作中的一道“关卡”或者“栅栏”。
想象一下,计算机在执行程序的时候,为了提高效率,可能会对指令的执行顺序进行一些调整。但是在多线程或者多核心的环境下,这种随意的调整可能会导致一些问题。
内存屏障的作用就是阻止这种随意的调整,确保特定的内存操作按照我们期望的顺序执行。
所以“内存屏障”本身只是一种“技术”,而这种“技术”可以实现很多“业务功能”。
这就像 Spring 中的 AOP 一样,AOP 是一种“技术”,而这种技术可以实现很多业务功能。例如,针对日志处理可以使用 AOP、针对用户鉴权可以使用 AOP 等,而内存屏障也是一样,我们可以使用内存屏障实现可见性的“业务功能”,也可以实现有序性的“业务功能”等。
3.volatile 适用场景
volatile 常见场景有以下两种:
状态标记
单例模式中的双重检查锁
具体来说。
3.1 状态标记
例如,在多线程环境中用于表示某个任务是否完成的标志变量,具体代码如下:
3.2 单例模式中的双重检查锁
4.volatile 局限性
volatile 并不能保证原子性,也就是并不能保证线程安全。
例如,对于 i++ 这样的操作,它不是一个原子操作,单纯使用 volatile 修饰 i 并不能保证线程安全。
课后思考
为什么双重效验锁一定要加 volatile?不是已经加锁了吗?
本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。
评论