深入理解并发编程中volatile关键字的作用

一、volatile概述

volatile是java并发编程中常用的一个关键字,在某些特定的情况下,开发人员通常用它去实现线程间的通信,而不是使用synchronized和Lock。因为在JVM的概念中,volatile是一种轻量级锁,使用它的消耗会远小于上述两种锁机制。当使用synchronized和Lock时会引起大量的线程上下文的切换。线程上下文切换:当处理机的一个时间片执行完成之时,而当前线程并未完成,那么便需要创建内存来保留当前线程中的指令和数据,等到下次争取到时间片以后,再次需要把这些指令和数据加载进来。显而易见,这是一个非常耗时和耗费资源的过程。而volatile则可以避免线程上下文的切换,为什么能做到以及如何做?下文见分晓

二、内存模型

既然提到了线程之间的通信,那么就必然离不开JMM(Java Memory Model)。在JMM中,将程序运行时的内存主要划分为主内存和工作内存两部分。主内存中主要包括一些共享的数据,资源,每个线程都有自己的工作内存,里面存储有主内存中的共享数据和资源的副本,线程对共享资源的操作都是在自己所占有的工作内存的变量副本(又称高速缓存)中完成的。当线程执行完成以后,便会将自己对资源所做的更改刷新到主内存中,这样其余的线程便可以访问到了。显然,线程如何从主内存中读取资源到工作内存,而后又将资源刷新到主内存便是一个必然要考虑到的问题。

三、JVM的原子操作

原子操作是指一系列不可中断的操作,Java中通过以下8个原子操作来实现资源在主内存和工作内存之间的交互:
lock:将对象变为独占模式
unlock:将锁住对象的锁释放
read:作用于主内存,将资源从主内存读取到工作内存
load:作用于工作内存,将资源从工作内存读取到工作内存中的变量副本之中
use:作用于工作内存,在变量副本中使用资源
assign:作用于工作内存,对变量副本中的资源进行更改赋值
store:作用于工作内存,将资源从变量副本卸载到工作内存
write:作用于主内存,将工作内存中的资源刷新到主内存
其中。read-load-use是将资源从主内存中读取到工作内存中不可中断的一系列操作,而assign-store-write则是刷新资源时的原子操作。到此,线程从主内存中读取数据,以及从工作内存刷新数据的问题已经解决了。试想,如果多个线程同时操作某一资源,如何一个线程对资源进行了更新,还未来得及刷新到主内存,另外的线程又在主内存中获取同样的资源,那么这必然是一份脏数据。Java编译器又是如何保证资源在线程之间的可见性的呢?

四、内存屏障

为了保证内存中资源的可见性,Java编译器在生成指令序列的时候,在适当的位置插入内存屏障指令,用以禁止特定类型的重排序,JMM中主要有一下4中内存屏障:
LoadLoad Barriers:确保Load1数据的装载要先于Load2及其所有后续数据的装载
StoreStore Barriers:确保Store1数据的卸载要先于Store2及其所有后续数据的卸载
LoadStore Barriers:确保Load1数据的装载要先于Store2及其所有后续数据的刷新
StoreLoad Barriers:确保Store1数据对于其它处理机的可见先于Load2及其随意后续数据的装载
其中StoreLoad具有前几种屏障的功能,但是它需要把所有写缓冲区中的资源,刷新到主内存中,因此需要慎用。下面我们回归主题,分析volatile的实现原理。

五、volatile原理

volatile主要作用是保证对象的可见性以及禁止指令重排序。注意,它并不能保证对象的原子性!
可见性:
被valotile修饰的变量在写操作的时候,会在编译之时在其中添加一条lock前缀指令。lock前缀指定会引起多核处理机发生以下两件事:
a:将当前处理机所缓存行的资源刷新到系统内存
b:上述的刷新操作会让其它地方缓存有该内存地址的资源失效
这样便可保证,每次线程对资源的更改在任何地方都是可见的
禁止指令重排序:
JMM在遇见由volatile修饰的对象资源之时,会在其适当的位置插入内存屏障以禁止指令重排序:
a:在每个volatile写之前插入一个StoreStore屏障(禁止volatile对象上面的普通写,与对它的volatile写重排序)
b:在每个volatile写之后插入一个StoreLoad屏障(禁止volatile对象的volatile写,与对它的volatile读重排序)
c:在每个volatile读之后插入一个LoadLoad屏障(禁止volatile对象的后序的读,与对它的volatile读重排序)
d:在每个volatile读之后插入一个LoadStore屏障(禁止volatile对象的后序写,与对它的volatile读重排序)
通过上述约定,便可禁止访问volatile对象的指令进行重排序

六、示例

下面以单例模式的双重检查锁为例,看看volatile的具体使用:
not singleton
这是一个错误的示例,在多线程的情况下,可能instance并未初始化完成就返回了。原因:一个对象的创建过程大体分为三步:
1:为对象分配内存空间
2:初始化对象
3:将对对象的引用指向1中分配的内存空间的地址
倘若一个线程获取对象锁执行完第一步之后,发生了指令重排序,去执行第三步,此时又有新的线程刚进入,第一次检查便发现instance不等于null,便返回。但是,对象并未完成初始化,所以返回得对象并未我们所需要的。
加上volatile:
singleton
当使用volatile关键字修饰以后,便不会发生上述的指令重排序过程,因此便可保证所有线程获取到的对象都是同一个实例。

注:本文是个人认识加上对参考书籍理解,若有不严谨之处,欢迎指正。^_^

参考书籍:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/jackFXX/article/details/81429959