Java中volatile详解 - pingdongyi/blog-2 GitHub Wiki
Java关键字volatile
用于将一个Java变量标记为 在主内中存储 ,更准确的解释为:每次读取一个 volatile
变量时将从电脑的主内存中读取而不是从CPU缓存中读取,每次对一个 volatile
变量进行写操作时,将会写入到主内存中而不是写入到CPU缓存中。
从Java5之后 volatile 关键字不仅能用于确保变量从主内存中读取和写入,事实上,volatile 关键字还有如下作用:
如果线程A写入了一个 volatile 变量然后线程B读取了这个相同的 volatile 变量,那么所有在线程A写之前对其可见的变量,在线程B读取这个 volatile 之后也会对其可见。 volatile 变量的读写指令不能被JVM重排序(出于性能的考虑,JVM可能会对指令重排序如果JVM检测到指令排序不会对程序运行产生变化)。 前后的指令可以重排序,但是 volatile 变量的读和写不能与这些重排序指令混在一起。任何跟随在 volatile 变量读写之后的指令都会确保只有在变量的读写操作之后才能执行。
尽管 volatile 关键字确保了所有对于 volatile 变量的读操作都是直接从主内存中读取的,所有对于 volatile 变量的写操作都是直接写入主内存的,但仍有一些情况只定义一个 volatile 变量是不够的。
在前面的场景中,线程1对共享变量 counter 写入操作,声明 counter 变量为 volatile 之后就能够确保线程2总是可以看见最新的写入值。
事实上,如果写入该变量的值不依赖于它前面的值,多个线程甚至可以在写入一个共享的 volatile 变量时仍然能够持有在主内存中存储的正确值。换句话解释为,如果一个线程在写入volatile共享变量时,不需要先读取该变量的值以计算下一个值。
一旦一个线程需要首先读取一个 volatile 变量的值,然后基于该值产生 volatile 共享变量的下一个值,那么该 volatile 变量将不再能够完全确保正确的可见性。在读取 volatile 变量和写入它的新值这个很短的时间间隔内,产生了一个 竞争条件 :多个线程可能会读取 volatile 变量的相同值,然后产生新值并写入主内存,这样将会覆盖互相的值。
这种多个线程同时增加相同计数器的场景正是 volatile 变量不适用的地方,接下来的部分进行了更详细的解释。
假设线程1读取一个值为0的共享变量 counter 到它的CPU缓存中,将它加1但是并没有将增加后的值写入主内存中。线程2可能会从主内存中读取同一个 counter 变量,其值仍然为0,同样不将其写入主内存中,就如下面的图片所展示的那样: p3.png
线程1和线程2现在都没有同步,共享变量 counter 的真实值应该是2,但是在每个线程的CPU缓存中,其值都为1,并且主内存中的值仍然是0。它成了一个烂摊子,即使这些线程终于它们对共享变量 counter 的计算值写入到主内存中,counter 的值仍然是错的。
就如在前面提到的那样,如果两个线程同时对一个共享变量进行读和写,那么仅用 volatile 变量是不够的。在这种情况下,你需要使用 synchronized 来确保关于该变量的读和写都是原子操作。读或写一个 volatile 变量时并不会阻塞其它线程对该变量的读和写。在这种情况下必须用 synchronzied 关键字来修饰你的关键代码。
由于 volatile 变量的读和写都是直接从主内存中进行的,相对于CPU缓存,直接对主内存进行读写代价更高, 访问一个 volatile 变量也会阻止指令重新排序,而指令排序也是一个常用的性能增强技术。因此,你应该在只有当你确实需要确保变量可见性的时候才使用 volatile 变量。