|
| 1 | +在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如`synchronized`、`volatile`、`final`、`concurren包`等。在[前一篇][2]文章中,我们也介绍了`synchronized`的用法及原理。本文,来分析一下另外一个关键字——`volatile`。 |
| 2 | + |
| 3 | +本文就围绕`volatile`展开,主要介绍`volatile`的用法、`volatile`的原理,以及`volatile`是如何提供可见性和有序性保障的等。 |
| 4 | + |
| 5 | +`volatile`这个关键字,不仅仅在Java语言中有,在很多语言中都有的,而且其用法和语义也都是不尽相同的。尤其在C语言、C++以及Java中,都有`volatile`关键字。都可以用来声明变量或者对象。下面简单来介绍一下Java语言中的`volatile`关键字。 |
| 6 | + |
| 7 | +### volatile的用法 |
| 8 | + |
| 9 | +`volatile`通常被比喻成"轻量级的`synchronized`",也是Java并发编程中比较重要的一个关键字。和`synchronized`不同,`volatile`是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。 |
| 10 | + |
| 11 | +`volatile`的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用`volatile`修饰就可以了。 |
| 12 | + |
| 13 | + public class Singleton { |
| 14 | + private volatile static Singleton singleton; |
| 15 | + private Singleton (){} |
| 16 | + public static Singleton getSingleton() { |
| 17 | + if (singleton == null) { |
| 18 | + synchronized (Singleton.class) { |
| 19 | + if (singleton == null) { |
| 20 | + singleton = new Singleton(); |
| 21 | + } |
| 22 | + } |
| 23 | + } |
| 24 | + return singleton; |
| 25 | + } |
| 26 | + } |
| 27 | + |
| 28 | + |
| 29 | +如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用`volatile`关键字修饰可能被多个线程同时访问到的singleton。 |
| 30 | + |
| 31 | +### volatile的原理 |
| 32 | + |
| 33 | +在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中我们曾经介绍过,为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。 |
| 34 | + |
| 35 | +但是,对于`volatile`变量,当对`volatile`变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。 |
| 36 | + |
| 37 | +但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现`缓存一致性协议` |
| 38 | + |
| 39 | +**缓存一致性协议**:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。 |
| 40 | + |
| 41 | +所以,如果一个变量被`volatile`所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个`volatile`在并发编程中,其值在多个缓存中是可见的。 |
| 42 | + |
| 43 | +### volatile与可见性 |
| 44 | + |
| 45 | +可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 |
| 46 | + |
| 47 | +我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。 |
| 48 | + |
| 49 | +前面的关于`volatile`的原理中介绍过了,Java中的`volatile`关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用`volatile`来保证多线程操作时变量的可见性。 |
| 50 | + |
| 51 | +### volatile与有序性 |
| 52 | + |
| 53 | +有序性即程序执行的顺序按照代码的先后顺序执行。 |
| 54 | + |
| 55 | +我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如`load->add->save` 有可能被优化成`load->save->add` 。这就是可能存在有序性问题。 |
| 56 | + |
| 57 | +而`volatile`除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。 |
| 58 | + |
| 59 | +普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。 |
| 60 | + |
| 61 | +volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被`volatile`修饰的变量的操作,会严格按照代码顺序执行,`load->add->save` 的执行顺序就是:load、add、save。 |
| 62 | + |
| 63 | +### volatile与原子性 |
| 64 | + |
| 65 | +原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。 |
| 66 | + |
| 67 | +我们在[Java的并发编程中的多线程问题到底是怎么回事儿?][3]中分析过:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。 |
| 68 | + |
| 69 | +在上一篇文章中,我们介绍`synchronized`的时候,提到过,为了保证原子性,需要通过字节码指令`monitorenter`和`monitorexit`,但是`volatile`和这两个指令之间是没有任何关系的。 |
| 70 | +
10000
|
| 71 | +**所以,`volatile`是不能保证原子性的。** |
| 72 | + |
| 73 | +在以下两个场景中可以使用`volatile`来代替`synchronized`: |
| 74 | + |
| 75 | +> 1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。 |
| 76 | +> |
| 77 | +> 2、变量不需要与其他状态变量共同参与不变约束。 |
| 78 | +
|
| 79 | +除以上场景外,都需要使用其他方式来保证原子性,如`synchronized`或者`concurrent包`。 |
| 80 | + |
| 81 | +我们来看一下volatile和原子性的例子: |
| 82 | + |
| 83 | + public class Test { |
| 84 | + public volatile int inc = 0; |
| 85 | + |
| 86 | + public void increase() { |
| 87 | + inc++; |
| 88 | + } |
| 89 | + |
| 90 | + public static void main(String[] args) { |
| 91 | + final Test test = new Test(); |
| 92 | + for(int i=0;i<10;i++){ |
| 93 | + new Thread(){ |
| 94 | + public void run() { |
| 95 | + for(int j=0;j<1000;j++) |
| 96 | + test.increase(); |
| 97 | + }; |
| 98 | + }.start(); |
| 99 | + } |
| 100 | + |
| 101 | + while(Thread.activeCount()>1) //保证前面的线程都执行完 |
| 102 | + Thread.yield(); |
| 103 | + System.out.println(test.inc); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + |
| 108 | +以上代码比较简单,就是创建10个线程,然后分别执行1000次`i++`操作。正常情况下,程序的输出结果应该是10000,但是,多次执行的结果都小于10000。这其实就是`volatile`无法满足原子性的原因。 |
| 109 | + |
| 110 | +为什么会出现这种情况呢,那就是因为虽然volatile可以保证`inc`在多个线程之间的可见性。但是无法`inc++`的原子性。 |
| 111 | + |
| 112 | +### 总结与思考 |
| 113 | + |
| 114 | +我们介绍过了`volatile`关键字和`synchronized`关键字。现在我们知道,`synchronized`可以保证原子性、有序性和可见性。而`volatile`却只能保证有序性和可见性。 |
| 115 | + |
| 116 | +那么,我们再来看一下双重校验锁实现的单例,已经使用了`synchronized`,为什么还需要`volatile`? |
| 117 | + |
| 118 | + public class Singleton { |
| 119 | + private volatile static Singleton singleton; |
| 120 | + private Singleton (){} |
| 121 | + public static Singleton getSingleton() { |
| 122 | + if (singleton == null) { |
| 123 | + synchronized (Singleton.class) { |
| 124 | + if (singleton == null) { |
| 125 | + singleton = new Singleton(); |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + return singleton; |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + |
| 134 | +答案,我们在下一篇文章:既生synchronized,何生亮volatile中介绍,敬请关注我的博客(http://47.103.216.138)和公众号(Hollis)。 |
| 135 | + |
| 136 | + [1]: http://47.103.216.138/archives/2550 |
| 137 | + [2]: http://47.103.216.138/archives/2637 |
| 138 | + [3]: http://47.103.216.138/archives/2618 |
0 commit comments