lht94
2021/09/07阅读:52主题:默认主题
volatile关键字
volatile的作用
Java的volatile关键字用于标记一个变量“应当存储在主存”。更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。
-
变量可见性和volatile的可见性保证
如果变量a没有声明volitale,如图中的变量a,线程a更改a的值后,并不能保证刷入主内存,就会出现cpu缓存和主内存不一致的情况,此时线程B来读取,可能会读取到线程A更改前的b。而如果声明为volatile,在线程A更改b的值后,会立即刷入主内存,并且会清除掉线程B的内存空间中的b缓存,线程B直接读取内存中的b变量。

volatile的完整性保证
实际上,volatile的可见性保证并不是只对于volatile变量本身那么简单。可见性保证遵循以下规则:
如果线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有可见的变量值,在线程B读取volatile变量后也同样是可见的。
如果线程A读取一个volatile变量,那么线程A中所有可见的变量也会同样从主存重新读取。
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
完整的volatile可见性保证意味着,在写入days变量时,线程中所有可见变量也会写入到主存。也就是说,写入days变量时,years和months也会同时被写入到主存。
public class MyClass {
private int years;
private int months
private volatile int days;
//volatile变量days读取放在最前,保证其他变量也从内存读取
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
//volatile变量days写入放在最后,保证其他变量也写入内存
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
注意totalDays()方法开始读取days变量值到total变量。在读取days变量值时,months和years的值也会同时从主存读取。因此,按上面所示的顺序读取时,可以保证读取到days、months、years变量的最新值。
volatile变量的读写会触发刷新操作,会影响其他变量的读取如上面所示。
指令重排问题
一个经典的使用场景就是双重懒加载的单例模式了:
public class Singleton {
//volatile修饰
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}
singleton=newSingleton();,这段代码其实是分为三步: (1)分配内存空间。 (2)初始化对象。 (3)将 singleton 对象指向分配的内存地址。
如果不用volatile修饰singleton ,可能会导致,指令重排,变成(1),(3),(2),另一个线程进来之后发现singleton不等于null,直接返回singleton,此时singleton并未初始化。
-
volatile的Happens-Before
volatile变量在读写操作前后添加内存屏障,禁止了指令的重排序,保证了有序性。
内存屏障有LoadLoad屏障,StoreStore屏障,LoadStore屏障,StoreLoad屏障。比如loadload屏障,加在Load1; Load2两个原子操作之间 ,保证在Load2及后续的读操作读取之前,Load1已经读取。其他同理。
在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad在每个volatile读取之前,插入LoadLoad,之后插入LoadStore。
volatile的重排序规则表:

(1)保证,volatile之后的所有读操作都不能在volatile读操作之前。
(2)保证,volatile之前的所有写操作都不能在volatile写操作之后。
-
volatile无法保证原子性,只能保证自身读写为原子操作。
volatile并不完全可靠
多个线程都能写入共享的volatile变量,主存中也能存储正确的变量值,然而这有一个前提,变量新值的写入不能依赖于变量的旧值。换句话说,就是一个线程写入一个共享volatile变量值时,不需要先读取变量值,然后以此来计算出新的值。
如果线程需要先读取一个volatile变量的值,以此来计算出一个新的值,那么volatile变量就不足够保证正确的可见性。(线程间)读写volatile变量的时间间隔很短,这将导致一个竞态条件,多个线程同时读取了volatile变量相同的值,然后以此计算出了新的值,这时各个线程往主存中写回值,则会互相覆盖。
正确使用volatile
如果两个线程同时读写一个共享变量,仅仅使用volatile关键字是不够的。你应该使用 synchronized 来保证读写变量是原子的。(一个线程)读写volatile变量时,不会阻塞(其他)线程进行读写。你必须在关键的地方使用synchronized关键字来解决这个问题。
volatile的性能考量
读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,你应当只在需要保证变量可见性的情况下,才使用volatile变量。
作者介绍