volatile实现原理及使用
近期在看《java并发编程的艺术》这本书,把自己的一些学习感悟给记录一下,当做是笔记啦!
volatile的作用
volatile保证了共享变量的可见性,也就是当一个线程修改这个共享变量时,另一个线程能够读到这个变量被修改之后的值。
volatile的实现原理
(1)CPU缓存和缓存行
我们要了解volatile的实现原理的话,首先先了解一下CPU Cache 和缓存行吧!
CPU在处理数据时,首先会访问缓存,若是缓存中没有数据(没有命中),那么才回去主内存中查找。
CPU在访问缓存的速度可以参考下面这幅图:
图来源:布鲁斯IO
因为一级缓存中的数据存储比较小,因此又引入了L2和L3,但是访问的速度相对于1级缓存来说会慢。
(2)volatile实现两条原则
1)Lock前缀指令会引起处理器缓存写回到内存中。
在使用了volatile关键字的变量中,编译成汇编语言之后会在操作这个关键字指令之前加上lock指令。lock指令会在执行的时候将修改的变量值,直接存储到其原来对应的地址中。
那么问题来了,每个处理器都有缓存,我们只是修改了主内存中的值,并没有修改别的处理器的缓存的值啊,那么不就会导致那个处理器缓存的值并没有修改吗?所以就可以引入下一条原则啦!
2)一个处理器的缓存写回到内存会导致其他处理器的缓存无效。
处理器之间会有一个缓存一致性协议,处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存数据保持一致。当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效缓存。当处理器需要使用到这个数据的时候,会重新从内存中读取进入缓存行中。
volatile使用问题—原子性
当自己学习到这个知识之后,想到之前学习java多线程可能都会讲的一个例子-----多线程抢票
在这个例子中,会有多个线程进行抢票,因此会存在并发问题。我就想,并发问题既然是因为每个线程读取不到对应其他线程修改的值而导致的,那么我加上这个volatile后会不会就解决了这个问题了呢?
package com.yangyang.thread;
/**
* 抢票窗口
*/
class Windows implements Runnable{
/**
* 当前票数
*/
public static int nums = 100;
@Override
public void run() {
// synchronized (Windows.class){
while (nums>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
nums--;
System.out.println("当前窗口:"+Thread.currentThread().getName()+" "+"当前票数:"+nums);
}
// }
}
}
public class TestMoreThread {
public static void main(String[] args) {
// 初始化窗口
Windows w1 = new Windows();
Windows w2 = new Windows();
Windows w3 = new Windows();
// 创建抢票窗口
Thread t1 = new Thread(w1, "窗口1");
Thread t2 = new Thread(w2, "窗口2");
Thread t3 = new Thread(w3,"窗口3");
// 开启抢票
t1.start();
t2.start();
t3.start();
}
}
如果我将
public static int nums = 100;
修改为:
public static volatile int nums = 100;
是不是可以代替synchronized这个锁呢?
但是发现并没有,结果是错误的。
这是为什么呢?
最后我在查看代码的时候,发现我的IDEA给了这样一个提示:
这段话的意思就是:nums–操作并不是原子操作。(顺便说一句:IDEA牛逼!!!!)
哦!问题来了,nums–其实是三个原子操作:
1)取到nums的值。
2)对nums的值进行加1操作。
3)将值写入到内存中。
要知道的是,在java中只有对基本的变量的复制和读取是原子操作的。例如i=1,这样是原子操作的!!!!
所以这个例子使用volatile的问题就是,当我的窗口1进行nums–这个操作时,还没有修改值,就阻塞而后被窗口2给读入了原来没有被修改的值。所以说这个场景的并发问题并没有得到解决。
文章若是有错误,请大家指正!!!
参考文章: