转载链接:http://blog.csdn.net/zero__007/article/details/44080975
一. volatite 简述
Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.
通俗来讲,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而对于普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
volatile很容易被误用,用来进行原子性操作。volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算不是线程安全的.
下面看一个例子,我们实现一个计数器,每次线程启动的时候,会调用计数器inc方法,对计数器进行加一操作。
1: public class Counter {
2: public static int count = 0;
3:
4: public static void inc() {
5: //这里延迟1毫秒,使得结果明显
6: try {
7: Thread.sleep(1);
8: } catch (InterruptedException e) {
9: }
10: count++;
11: }
12:
13: public static void main(String[] args) {
14: //同时启动1000个线程,去进行i++计算,看看实际结果
15: for (int i = 0; i < 1000; i++) {
16: new Thread(new Runnable() {
17: @Override
18: public void run() {
19: Counter.inc();
20: }
21: }).start();
22: }
23: //这里每次运行的值都有可能不同,可能为1000
24: System.out.println("运行结果:Counter.count=" + Counter.count);
25: }
26: }
运行结果:Counter.count=995。可以看出,在多线程的环境下,Counter.count并没有期望结果是1000。很多人以为,这个是多线程并发问题,只需要在变量count之前加上volatile就可以避免这个问题,那再修改代码看看。
1: public class Counter {
2: public volatile static int count = 0;
3:
4: public static void inc() {
5: //这里延迟1毫秒,使得结果明显
6: try {
7: Thread.sleep(1);
8: } catch (InterruptedException e) {
9: }
10: count++;
11: }
12:
13: public static void main(String[] args) {
14: //同时启动1000个线程,去进行i++计算,看看实际结果
15: for (int i = 0; i < 1000; i++) {
16: new Thread(new Runnable() {
17: @Override
18: public void run() {
19: Counter.inc();
20: }
21: }).start();
22: }
23: //这里每次运行的值都有可能不同,可能为1000
24: System.out.println("运行结果:Counter.count=" + Counter.count);
25: }
26: }
运行结果:Counter.count=992,还是没有我们期望的1000,下面分析一下原因。
自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
假如某个时刻变量count的值为10,线程1对变量进行自增操作,线程1先读取了变量count的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量count的原始值,由于线程1只是对变量count进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量count的缓存行无效,所以线程2会直接去主存读取count的值,发现count的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了count的值,注意此时在线程1的工作内存中count的值仍然为10,所以线程1对count进行加1操作后count的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,count只增加了1。
解释到这里,可能会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对count值进行修改。然后虽然volatile能保证线程2对变量count的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
在 jvm虚拟机中,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这写交互。
read and load 从主存复制变量到当前工作内存;use and assign 执行代码,改变共享变量值;store and write 用工作内存数据刷新主存相关内容。
其中use and assign 可以多次出现,但是这一些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6,线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6,导致两个线程即使用volatile关键字修改之后,还是会存在问题。
二.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
1: //线程1
2: boolean stop = false;
3: while(!stop){
4: doSomething();
5: }
6:
7: //线程2
8: stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程。
下面解释一下这段代码为何有可能导致无法中断线程。每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
三.synchronized与volatile
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1. 状态标记量
1: volatile boolean flag = false;
2: while(!flag){
3: doSomething();
4: }
5: public void setFlag() {
6: flag = true;
7: }
8:
9:
10: volatile boolean inited = false;
11: //线程1:
12: context = loadContext();
13: inited = true;
14: //线程2:
15: while(!inited ){
16: sleep()
17: }
18: doSomethingwithconfig(context);
2.double check
1: class Singleton{
2: private volatile static Singleton instance = null;
3:
4: private Singleton() {
5: }
6:
7: public static Singleton getInstance() {
8: if(instance==null) {
9: synchronized (Singleton.class) {
10: if(instance==null)
11: instance = new Singleton();
12: }
13: }
14: return instance;
15: }
16: }
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。