众所周知,volatile关键字可以让线程的修改立刻通知其他的线程,从而达到数据一致的作用。那么它具体涉及到哪些内容呢?
众所周知,volatile关键字可以让线程的修改立刻通知其他的线程,从而达到数据一致的作用。那么它具体涉及到哪些内容呢?
关于缓存
计算机最大的存储空间就是磁盘(硬盘),但是访问的速度也是最慢的,价格最便宜;再就是内存,容量更小,造价更高,但是速度也更快。不过跟cpu的计算速度比起来,那就太慢了。可以想像,如果cpu每次计算都要从内存读取数据,那大部分的时间估计都浪费在这上面了。所以就引入了缓存的概:
缓存的结构大概时这样的,从1级到3级速度越来越慢,最后通过总线与内存连接。如果时多核多cpu,那么结构大概是这样的:
多线程造成的缓存不一致
由于现在大部分的机器都有多个cpu,这就导致如果时运行多线程的任务,就可能运行在不同的cpu上。试想一下:1
2
3int a = 0;
int b = a;
b += 1;
如果开启两个线程执行,我们想要的结果是3,但是最后的结果只是2。这是因为在做加法运算的时候,cpu会先把a的值读入cpu的缓存,然后更新缓存,在更新内存。很有可能两个线程分散在两个cpu,每个都是对自己缓存内的数据进行读写,这样就造成了结果不一致的现象。
volatile的作用
volatile的作用就是当一个线程更新某个volatile声明的变量时,会通知其他的cpu使缓存失效,从而其他cpu想要做更新操作时,需要从内存重新读取数据。具体的通知方式,一种是通过某种协议,比如MESI;再就是对总线加锁,控制变量的读取。具体硬件上怎么个流程,我就搞不清楚了…
并发
这里还需要强调的时,并发编程涉及的三个特性:原子性、可见性、有序性。就好像分布式里面的cap一样,需要熟知。先来通俗的描述下:
原子性
即要么全做,要么全部做。比如从a银行转钱到b银行。
在编程中,除了long或者double外的变量更新就是原子操作。long和double除外,是因为它们在32位的操作系统上,会被分成两部分进行更新,此时就不是原子的。
再比如最常见的i++也不是原子的,它相当于先读取i
,进行+1操作
,更新
三个步骤进行。
可见性
多个线程访问同一个变量时,这个变量被修改后,能被其他的线程看到。
有序性
比如1
2
3
4int a = 10;
int r = 2;
a = a + 3;
r = a*a;
这段代码有可能进行指令的重排,从而导致结果跟预期的不一致。指令的重排需要按照happens-before
原则,比如:
- 程序次序原则,一个线程内,按照书写的顺序执行
- 锁定原则,lock前后执行
- volatile原则,volatile变量前后执行
- 传递原则,如果a需要调用b,那么a就会在b的前面
…
等等…
volatile的特性
volatile只能保证变量的可见性、有序性,但是不能保证原子性。因此可以用它来做double-check,但是不能来做i++的操作。如果想要实现i++的可靠性,必须依赖于synchronized、lock或者atomicXXX来实现。
参考
- 海子的《Java并发编程:volatile关键字解析》:http://www.cnblogs.com/dolphin0520/p/3920373.html
- liuxiaopeng的《Java 并发编程:volatile的使用及其原理》:https://www.cnblogs.com/paddix/p/5428507.html
- double check http://blog.csdn.net/dl88250/article/details/5439024
- cpu缓存知识:http://blog.jobbole.com/36263/