本篇是对Java并发编程:volatile关键字解析的学习笔记
一、相关原理说明
1. volatile保证可见性
- volatile修饰的变量被一个线程修改后,立即写入到主内存中
- volatile修饰的变量被修改时,将其他线程中该变量的副本拷贝设置为失效
- 当其他线程需要使用该变量时,由于工作内存中的副本拷贝失效,故需重新到主内存中获取
普通共享变量,被线程修改后,修改后的值刷新到主内存的时机不定,被volatile修饰的共享变量被修改后可立即刷新到主内存。
2. volatile禁止指令重排序
????
3. volatile不能保证原子性
public class VolatileTest {
// 共享变量
private volatile int inc=0;
public void add(){
inc++;
}
public static void main(String[] args) {
final VolatileTest test=new VolatileTest();
// 开10个线程,在每个线程中执行inc++1000次
for(int i=0;i<10;i++){
new Thread(){
public void run(){
for(int j=0;j<1000;j++){
test.add();
}
}
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
最后输出的数肯定是一个<10000的数
造成的原因:可能存在的情况是
public void increase() {
inc++;
}
方法进行等价转换,方便解释说明
public void increase(){
int tmp = inc; //1
tmp = tmp + 1; //2
inc = tmp; //3
}
假设当前inc=0
线程1:将inc=0读取到工作内存赋给temp=0——>阻塞……
线程2:将inc=0读取到工作内存——>inc++得到1写入到工作内存——>将inc=1立即写入到主内存(由于volatile的作用),并且线程1中inc已经是无效的地址了,但此时inc有效无效都已经对tmp变量没有任何影响了。
线程1:继续执行tmp = tmp + 1; inc = tmp; ——>将inc=1立即写入到主内存(由于volatile的作用),并且其他线程中inc已经是无效的地址了
4.volatile可在一定程度上保证有序性
??
二、volatile使用场景
1. 状态标记量
示例1(利用了 volatile保证了可见性)
// 共享变量
volatile boolean flag=false;
// 线程1
while(!flag){
//dosomething
}
// 线程2
public void setFlag(){
flag=true;
}
共享变量不用volatile修饰时
可能出现的情况:线程2将flag在工作内存中设置为true,但并没有刷新到主内存中,线程2转而去做其他的事了,这样就造成线程1停止不了。
共享变量用volatile修饰时
线程2将flag在工作内存中设置为true,由于volatile的作用使得该修改能够立即刷新到主内存,并将线程1中flag的缓存设置为无效,当线程1需要读取flag的值时去主内存中读取变量最新的值。
示例2(利用了 volatile禁止指令重排)
// 共享变量
volatile boolean isinited=false;
// 线程1
context=loadContext();// 语句1
isinited=true;// 语句2
// 线程2
while(!isinited){
sleep
}
dosomething(context);
共享变量不用volatile修饰时
可能出现的情况:由于编译器、处理器的指令重排,使得线程1的语句2先于语句1执行,线程2读取到isinited=true时开始执行dosomething()的代码,由于context为空,导致程序执行错误。
共享变量用volatile修饰时
??
2. 双重检查锁定(利用了 volatile禁止指令重排)
public class Singleton3 {
private Singleton3() {
}
/**
* 这里要用volatile:防止new Singleton3()操作的指令重排序导致的错误
* (JDK1.5以后这种错误才因volatile的出现得以避免;JDK1.5之前,毛的办法)
*/
private volatile static Singleton3 instance = null;
public static Singleton3 getInstance() {
// 这样的好处是:在实例还未创建时需要加锁,创建以后则不需加锁了
if(instance==null){
synchronized(Singleton3.class){
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
instance=new Singleton();
这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1.给Singleton实例 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance变量指向分配的内存空间(执行完这步 instance 就为非 null 了)。
instance不用volatile修饰时
可能出现的情况:主要在于instance = new Singleton()这句,
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
instance用volatile修饰时
??