java并发- volatile关键字
文章目录
- 基础
- 什么是volatile
- 使用 volatile 的困惑
- volatile对指令重排的影响\volatile与内存屏障
- volatile与synchronized区别
基础
-
可见性:
是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。在 Java 中 volatile、synchronized 和 final 实现可见性。
-
原子性:
原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
-
有序性:
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
什么是volatile
我们声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。靠 volatile 语义来搞定可见性的。
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
使用 volatile 的困惑
参考URL: https://time.geekbang.org/column/article/84017
例如下面的示例代码,假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?
直觉上看,应该是 42,那实际应该是多少呢?这个要看 Java 的版本,如果在低于 1.5 版本上运行,x 可能是 42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。
// 以下代码来源于【参考1】class VolatileExample {int x = 0;volatile boolean v = false;public void writer() {x = 42;v = true;}public void reader() {if (v == true) { // 这里x会是多少呢? } }}}}
分析一下,为什么 1.5 以前的版本会出现 x = 0 的情况呢?我相信你一定想到了,变量 x 可能被 CPU 缓存而导致可见性问题。这个问题在 1.5 版本已经被圆满解决了。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。怎么增强的呢?答案是一项 Happens-Before 规则。
Happens-Before 规则, 真正要表达的是:前面一个操作的结果对后续操作是可见的。
Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
-
程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这还是比较容易理解的,比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。 -
volatile 变量规则
这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。
- 传递性这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
volatile对指令重排的影响\volatile与内存屏障
指令重排:JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序,为的是不改变程序结果条件下优化效率,注意这里不改变程序结果指的是单线程的情况下。
内存屏障:内存屏障(Memory Barrier)是一种CPU指令,意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
在一个变量被volatile修饰后,对应的读写操作前会被插入内存平展,编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
volatile与synchronized区别
1、volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2、volatile仅能使用在变量级别;synchronized使用在方法、和类级别、代码块。
3、volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化