文章目录
volatile
volatile修饰的变量,其进行读写操作时都是从主存访问,而不是Cache
volatile解决了什么问题
一、变量的可见性问题
在多线程应用中,如果变量没有被volatile修饰,那么每个线程可能从主存中复制变量到CPU的缓存里。而且如果你的电脑是多核处理器,那么有可能会出现每个单核中的线程都复制一次变量。可以通过图来理解下:
也就是说,对于没有被volatile修饰的变量,虚拟机无法保证线程数据是读取从主存读取到cache,写入操作是从cache到主存。那么,这会带来什么问题呢?
让我们假设一个场景,有多个线程能够对同一变量进行操作,变量声明如下:
public class SharedObject {
public int counter = 0;
}
此时,只有Thread1对counter变量进行加1操作,同时其他线程读取counter的值。由于counter没有被volatile修饰,也就意味着修改的变量不一定能够即时的从cache写回到主存中。这就可能导致主存和cache中counter的值不一致。如图中,可能Thread1已经将值修改为了7,但是没有即时写回到主存中,Thread2读的时候就还是读到0。这样在Thread2中的计算就会以0为初始值进行计算。
上面这个例子中,因为一个线程没有即时将值写入主存而导致其他线程无法获取到最新值的问题,称为“可见性问题”。
二、volatile解决可见性的问题
这个问题的答案显而易见,也是我们这篇文章的中心,即用volatile修饰变量。
public class SharedObject {
public volatile int counter = 0;
}
加上volatile修饰符后,线程在进行写操作时会将修改后的值即时同步到主存中。那在我们上面的场景中(只有一个线程修改变量,其他线程只是读取变量并不修改),可见性的问题能够得到有效解决。即一个线程修改变量后,其他线程读取时是读取到最新的值。
到这一步就能够满足我们的要求,显然是不够的,如果多个线程都对一个变量进行修改那要怎样保证变量的即时更新。
三、充分的volatile可见性保证(Full Volatile Visibility Guarantee)
上面一节介绍了下volatile的解决可见性问题的原理,那么可见性具体有哪些场景呢?
- 一个被volatile修改的变量variable,ThreadA对variable进行写操作,之后ThreadB对variable进行读操作。那么对于ThreadA写操作之前可见的所有变量,在ThreadB读操作之后也对ThreadB也是可见的。
- 同样是被volatile修饰的变量,当ThreadA读取variable,那么在ThreadA读取variable变量时所有可见的变量之后将从主存中重新读取。
上面的概念有些抽象,下面通过一个例子来具体说明下:
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
这个实例中,只有days是被volatile修饰的。
上述的两个概念具体起来就是:当ThreadA对volatile修饰的变量进行写操作时,做一个update()操作对其余变量也进行写操作,把值写入到主存。当ThreadA进行读操作,读取days的值时,做一个totalDays操作,将其他值也读取到主存中,以此保证获取最新的值。
四、指令重排
volatile除了解决变量可见性的问题,还能解决指令重排的问题。
指令重排:为了性能,JVM和CPU允许指令重新排序,只要语义不变。
语句一:
int a = 1;
int b = 2;
a++;
b++;
语句二:
int a = 1;
a++;
int b = 2;
b++;
语句一和语句二虽然语句顺序不完全一样,但是所标的的含义都是一样的。我们还是基于MyClass这个案例来讲,这里就在重新写一遍这个类。
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;
}
}
上面的update()方法,当days被写入新值时,years和months也会将值写入到主存中,但是如果重排指令如下:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
days放在最前面赋值,当days值改变时,months和years依旧会把值写入到主存,但是是这个时候就无法知道days的改变了。
针对这个问题,解决办法是什么呢?
五、Java Volatile发生之前保护(The Java volatile Happens-Before Guarantee)
现在,我们可以知道指令重排也不是能够随便进行的,volatile能够限制某些情况下指令重排的发生。那么,哪些情况下指令不能进行重排呢?
- volatile修饰变量的写操作:一个被volatile修饰的变量days,如果其他变量读/写操作最初发生在写days之前,那么指令重排时就不能将对其他变量的读写操作重排至对days写操作的后面。示例如下:
// 原始操作,对days和months的读/写操作在days之前
// 那么重拍时是不能把对days和months的操作放在days后面的
public void update(int years, int months, int days){
this.months = months;
this.years = years;
this.days = days;
}
// 该命令重排是错误的
public void update(int years, int months, int days){
this.months = months;
this.days = days;
this.years = years;
}
但是可以反过来
// 一开始就在days写操作的后面
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
// 重排命令,改成在days前面,这种情况是允许的
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
- volatile修饰变量的读操作:一个被volatile修饰的变量days,如果其他变量读/写操作最初发生在读days之后,那么指令重排时就不能将对其他变量的读写操作重排至对days读操作的前面。
// 如果一开始读取months,years在days后面,那么是不能重排指令
// 将months,years的读取放在days读取的前面
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
// 这个指令重排是错误的
public int totalDays() {
int total = 0;
total += months * 30;
total += years * 365;
total += days;
return total;
}
总结
- volatile在只有一个线程写操作,其他线程读操作的情况下是线程安全的
- 当有多线程需要对变量进行修改时,还需要加上synchronized(虽然volatile保证了变量直接从主存读取,但是变量的一些复合操作是【读/修改/写】的一个循环操作)
- 应用场景
1. 原子的读写long和double类型的变量
2. 通知编译器某个特定字段将被多线程访问,而不对其进行指令重排
3. 用来完成一些同步功能,多个线程读取某变量最新的状态后进行下一步操作
感谢各位浏览,有什么不正确的地方还请指点。