volatile关键字和synchronized关键字的区别
本文介绍了volatile关键字和synchronized关键字的区别
一句话概括
volatile保证了线程间的可见性和有序性
synchronized保证了线程间的原子性(同时保证了可见性和有序性)
Java内存模型
首先,为了说明两者的区别,先要引出一个概念——Java内存模型(JMM),该模型用于屏蔽各种硬件和操作系统带来的内存访问的差异,定义了程序中各个变量**(不包括局部变量和方法参数,因为这些是线程私有的)的访问规则。
JMM规定所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
需要注意的是,这里Java内存模型中的主内存,工作内存和Java内存区域中的Heap,Stack,方法区等是没有关系的,如果非要类比,主内存可以对应Heap中的对象实例的数据部分,工作内存对应JVM Stack中的部分区域。
volatile语义
volatile
关键字可以说是Java虚拟机提供的最轻量级的同步机制。
当一个变量定义为volatile
时,它将具备两种特性:
第一,保证此变量对所有线程的可见性。这里的可见性指的是当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。普通变量不能做到这一点,普通变量的值在线程间传递都需要通过主内存来完成。
第二,禁止指令重排序优化。
举例说明
如下图所示,有两个线程t1和t2,它们都需要使用对象A中的变量flag,这两个线程都会从主内存中copy一份对象A的变量flag,放到它们各自的线程工作内存中,然后在运行期间直接使用这份copy。
当t2线程需要修改flag的值,它会先把自己工作区的flag修改然后存入主内存中,但是t1并不会去访问heap中的flag,而是依旧使用自己工作区的copy。
当volatile关键字作用到变量flag上,将会强制所有线程都去主线程中读取变量flag的值。
示例代码
1 | /** |
以上代码开启了一个线程t1,该线程执行m方法,如果不加volatile
关键字,在主线程中更改flag的值,并不会使循环停止,这是由于t1线程执行时使用的是来自主线程的拷贝,并不会在执行时访问主线程,而volatile
关键词使得主线程对flag的修改对于线程t1来说是可见的,t1会刷新flag的值并结束循环。
使用场景
由于volatile
变量只能保证可见性,在不符合以下两条规则的场景中,我们仍需要通过加锁(使用synchronized
或java.util.concurrent
中的Atomic类)来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
前面举的列子就很适合使用volatile变量
来控制并发,当shutdown()
被调用时,能保证所有线程中的m()方法立即停止。
大多数情况下使用volatile
比synchronized
开销低,选择的唯一依据就是我们是否在保证可见性和有序性的基础上,还需要保证原子性。