关键字之volatile
一、前言
昨晚心血来潮了解了一下volatile关键字,了解之后,内心迫不及待想要记录自己的所见所感。
说到这个关键字,会牵扯到内存之间的 交互,主内存与工作内存,以及并发编程中牵扯线程安全的时候的三大性质:原子性,有序性,可见性。
volatile :[ˈvɒlətaɪl],易变的,不稳定的
字面理解:加了此关键字的变量会被多个线程所改变,初步感知会跟static很相反。
下面会从那个透过三大性质来解读此关键字的作用。
二、三大性质解读其作用
1.可见性
当一个变量被附加上volatile关键字的时候,若一个线程改变它的值,那么其他线程可以立即访问到它改变之后的值并且也可以改变他。
(1)Java内存模型:主内存与工作内存
Java的内存模型规定了所有变量都会放置在主内存,每个线程都有自己的工作内存,线程不能直接访问主内存,而是通过访问自己工作内存,取出变量内容。
当线程需要取出一个数据的时候,会有一条指令发到工作内存说需要这个变量,cpu接收到指令之后就会去主内存访问取出数据放在线程的工作内存。
当线程要修改数据的时候,会先修改工作内存的数据内容,然后执行Sava指令,存到主内存。
(2)缓存不一致性问题解决
看到上面访问数据和写数据,那么大家可能会有这么一个疑问,比如主内存的"i=0",线程1访问到i=0,并令i=i+1,即i=1,写到了工作内存,但是此时线程2访问主内存得到i=0,并使i=i+1,接着线程1工作内存的i=1,写到了主内存i=1,最后线程2将工作内存里的i写到主内存,所以最终结果的i=1。
int i=0;
线程1
i=i+1;
线程2
i=i+1;
i=1,这显然是不对的,所以这就是缓存不一致的问题了,一般来说,会有两种方法去处理这个问题。
i.lock
同理看到上面的代码,当线程1在执行读取更改i值的时候,在总线上加上LOCK#锁,然后线程2就无法访问主内存,直到更改完成就解开锁,但是这个方法,就会引起被锁在外面的cpu一直等待着,这使得效率极大地降低
ii.缓存一致协议
Intel 的MESI协议,当访问工作内存的共享变量的时候,就告诉其他的cpu的将该变量的缓存行置为无效状态,直到写完数据给主内存,当其他的cpu访问到缓存行无效的时候,就会重新读取内存的共享变量。
2.原子性
尽管上面的缓存一致协议看起来已经很完美了,但是,我们可以看到下面的测试样例。
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
我们运行这段代码的时候,并不能得到我们预想中的答案,问题就是,volatile关键字并不能保证原子性,counter++并不是原子操作,原子操作有八种(load,read,lock,unlock,assign,store),当线程1读取counter值到工作内存的时候,还没来得及改变,其他线程就对他进行了增加操作,那么等线程1增加之后的值重新写入到主内存,这个值就会跟只相当于增加了一次,所有结果肯定不是我们想的那样。
如果说得更加具体一点就是:对a,b的访问:read a,read b,load b,load a。
**规则:**若是需要volatile保证原子性,那么就需要满足:
1.运算结果并不会依赖变量的当前值,或者能够确保只有一个线程对变量进行修改;
2.变量不需要与其他状态共同参与不变约束。
3.有序性
volatile关键字能屏蔽指令重排序
如下面代码
public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton();,这行代码需要三步执行:(1)先分配对象内存空间;(2)初始化对象;(3)设置instance指向刚刚分配好的地址。
但是若是没有添加volatile关键字,线程1就很可能执行,1->3->2,当线程2访问instance是否为null的时候,就直接null,然后又开始新建。
参考:三大性质总结:原子性,有序性,可见性
上一篇: 局部内部类访问局部变量的问题