一、volatile 特性
多线程环境下,每个线程都有自己的一个工作内存,对于共享变量,读时是先从主内存加载到工作内存,写时是从工作内存写回主内存。
这个工作内存就相当于一个线程级别的缓存,与其他缓存一样,存在一致性问题,比如一个共享变量的值在一个线程的工作内存中被修改了,那么什么时候同步回主内存呢?这个是没有明确规定的,这就导致了问题,例如多个线程都在自己的工作内存中修改共享变量的值,而主内存中的值是旧的,各个线程也不知道其他线程对变量的改动,这就是不可见问题。
而volatile可以保证:
- 每次读取volatile变量时都是从主内存中读的。
- 每次写volatile变量时都会写回主内存。
从而,volatile 解决了可见性问题,每次读写都是直接关联主内存的,这是 volatile 的首个重要特性。还有一个重要特性是防止指令重排序,具体的后面再说。
小结一下volatile的特性:
- 保证了共享变量在多线程下的可见性。
- 防止指令重排序。
二、volatile 是怎么保证可见性的?
1. 什么是可见性问题?
多线程应用中,为了性能,每个线程都会把共享变量从主内存拷贝到自己的工作内存中,多cpu计算机中每个线程运行在不同的cpu中,每个线程就把共享变量拷贝到自己的cpu cache中。
image对于普通变量,JVM什么时候将其从主内存读到 cpu cache 中?以及什么时候从 cpu cache 写回主内存?这些都是没有保证的。
public class SharedObject {
public int counter = 0;
}
如这段代码,假设线程1对counter执行增加操作,线程1和线程2都会时不时的读取 counter,线程1的操作结果什么时候写回主内存是没谱的,就会发生下图中的情况:
image可见性问题:一个线程修改了变量值,由于还没有写回主内存,导致其他线程看不到最新值,也就是一个线程的更新对其他线程不可见。
2. volatile 对可见性的保证
变量声明了 volatile 之后,所有对其写后都会立即写回主内存,所有对其的读操作都会直接从主内存中读。
大概原理:
多处理器下,会实现一个缓存一致性协议,每个处理器通过分析在总线上传播的数据来检查自己缓存的值是否过期了,所以当 volatile 变量值写回到主内存后,其他处理器会发现自己缓存行对应的内存地址被修改了,就会将当前缓存置为无效状态,所以当再次操作这个变量时就得从主内存重新读取到缓存,从而拿到最新值。
3. 全可见性保证
事实上,volatile 对可加性的保证已经超过了 volatile 变量本身,还遵循如下原则:
- 如果线程A对一个volatile变量进行写入,并且线程B接下来对这个变量进行读取,那么在写volatile变量前对线程A可见的所有变量,在线程B读取volatile变量之后这些变量对线程B也是可见的。
- 如果线程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;
}
}
udpate()
写了3个变量,其中只有 days
是 volatile
。
当days
写入后,所有可见变量(years、months)也会写入主内存。
读取的示例代码:
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;
}
}
totalDays()
中读取days
时,years、months
也会从主内存中读取。
三、volatile 是怎么防止重排序的?
为了提高性能,JVM和CPU允许对指令的顺序进行重排,只要保证整体语义和结果是正确的。
例如:
int a = 1;
int b = 2;
a++;
b++;
可以重拍为:
int a = 1;
a++;
int b = 2;
b++;
看下面的代码:
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
的写入被排在了第一行,虽然写入的同时也会把years months
的值写入主内存,但之后又修改了years months
的值,这时他俩的最新值就不会被马上写入主内存了,这就与重排序之前的代码意义不同了。
为了解决重排序问题,volatile 给出了“happens-before”保证:
- 对于其他变量的读写,如果本来是在写volatile之前,那么不能被重排到之后。volatile变量写之前的读写操作可以保证发生在写之前。注意,对于本来是在写volatile变量之后的其他变量读写操作是可以被重排到写volatile变量之前的。把后面的重排到前面可以,但把前面的排到后面是不允许的。
- 对于其他变量的读写,如果本来是发生在读volatile之后的,那么不能被重排到之前。注意,对于本来发生在读volatile之前的其他变量的读可以重拍到之后。把前面的读放到后面可以,但把后面的放到前面不行。
参考:http://tutorials.jenkov.com/java-concurrency/volatile.html
网友评论