Java中volatile关键字
volatile的用法
在java中,为了保证多线程并发中的原子性问题、可见性问题和有序性问题。java语言定义了很多相关的关键字,比如说synchronized、volatile、final
等。本文主要说明的就是volatile
关键字
volatile
一般可以理解成“轻量级别的synchronized
”,这是因为其有些功能和synchronized
相似。但和synchronized
不同的是,volatile
只能修饰变量,不能修饰方法或代码块。
而它的用法也很简单, 只需要在一个可能被多线程同时调用的变量前使用volatile
修饰即可。如下面使用双重锁校验的单例模式:
public class Singleton{
private volatile Singleton single;
private Singleton(){}
public Singleton getSingleton(){
if(single == null){
synchronized(Singleton.class){
if(single == null){
single = new Singleton();
}
}
}
return single;
}
}
如上代码是一个volatile
关键字的简单用法示例,至于为什么使用双重锁校验,和为什么使用了volatile
的同时又使用synchronized
等相关问题,会在其他文章中解释,这里不多做赘述。
volatile的原理
说到原理,可能要稍微说一下计算机内存和缓存的相关知识。
大家应该知道,计算机主要的运算处理等都是在CPU中进行,而数据是存储在内存中。这里我们将计算机的物理内存称为主内存。CPU在处理数据的时候要和内存交互,去读取内存。
但随着技术的发展,CPU的处理速度一直在增加,导致去内存中读写的速度跟不上CPU的处理速度了。这时候,缓存技术就应运而生。
缓存就是保存一份主存中部分数据的备份,这部分数据正式CPU所要处理的。缓存的特点是速度快、内存小和比较贵。有了缓存,CPU处理数据就只需要去和缓存交互,当处理完了,
缓存会把数据在刷写入主存当中。就相当与在主内存和CPU中间有增加了一个高速的中介,那就是缓存。
但随着技术的进步,一级的缓存速度也不够了,这时候就出来的二级缓存,甚至是三级缓存。总之,道理大概都一样,速度更快,内存更小,价格更贵。每一级缓存都是存储了上一级的部分数据。
好的,由于缓存的出现,也带来了一些问题。因为,最开始的时候,只有主内存的时候,比如说现在有多个线程,每个线程访问的数据都是在主存中。这不会有什么问题,因为每个线程访问的都是同一个
变量。这个变量对所有线程都是可见的。比如说,一个变量初始值为1,线程1进行操作将其改为2了,这时候线程2是知道这个值变为2了的。但现在引入了缓存技术,缓存是这样的,单核的CPU问题还好,
因为单核的只有一套缓存,所有线程共用这套缓存,那数据的可见性也是可以保证的。现在问题是多核CPU下的多线程,因为在多核下的缓存是这样的:每个核有自己的一级缓存或者一级和二级,然后共享
三级缓存和内存。打个比方,现在比如说有多核CPU比较高级,拥有3级缓存。那现在是这样的,每个核有自己的一级和二级缓存。三级缓存是共享的,而三级缓存和主存交互。如下图:
这时候,如果其中一个核上的线程更改了数值,这时候另外的核上缓存是不知道的(不可见)。比如,在主存中有变量a = 1
;这时候core1中执行了a += 1
;而core2中执行的是a += 2
.
比如core1中先执行,因为处理过程两个核有自己的一级和二级缓存,这时候其实a已经等于2了,而core2中的L1保存的a还是等于1,所以他执行后会将a置为3,而实际我们想要的结果应该是a值为4。
这个就是缓存一致性问题。
而除了上面说的缓存一致性问题,在大多数处理器中,还存在乱序执行的问题,就是处理器为了更充分的使用资源,会进行处理器优化。而很多语言的编译器也会有类似的有话,比如说java虚拟机的
JIT(即时编译器)就会做指令重排操作。
看了这些物理层面上的理论,都有点儿忘了自己要说什么了。好了,现在让我们将其与今天的问题结合起来。其实在并发变成的过程中,我们关注的的是变量的可见性问题、原子性问题和有序性问题。
而上面是说的缓存一致性问题所对应的就是可见性问题,而处理器优化乱序执行可能会导致原子性问题,指令重排导致的就是有序性问题。
现在铺垫的差不多了(其实还应该说一下java的内存模型,有兴趣的可以自行查阅资料),我们来说一下volatile的执行原理。
当使用volatile关键字修饰变量,当修改变量时,java虚拟机会向处理器发送一个lock前缀的指令,会让这个存于缓存中的值,马上回写到主存中。因为java的内存模型规定,每个线程只操作自己的工作内存,
工作内存之间是不可见的,变量的交互只能通过主存来传递。这里边有一个缓存一致性协议。
说白了就是,当使用volatile关键字修饰变量时,变量修改后会马上刷写到主存中,而要使用该变量时,必须先重新从主存中读取该变量到缓存中。这样就解决了缓存一致性问题。
volatile_可见性问题
上面说了,volatile的机制,它可以解决缓存一致性问题,也就是它是能保证线程执行过程中,变量是可见的。
volatile可以解决可见性问题。
volatile_有序性问题
前面我们提到,JIT进行指令重排时,可能会导致代码的执行顺序出现问题。比如,要执行的顺序是creat->search->delete
,而由于指令重排,导致真正的执行顺序为search->delete->creat
这肯定是我们不能接受的。而volatile
关键字可以有效的避免这个问题,因为它是可以禁止指令重排的。
volatile可以解决有序性问题。
volatile_原子性问题
所谓原子性,简单来说就是一个操作是不可中断的,要不执行完毕,要不不执行。java中的synchronized
中是使用字节码指令monitorenter
和monitorexit
来保证原子性的。而volatile
关键字与这两个指令没有任何关系。所以其无法解决原子性问题。
volatile无法保证原子性
总结
本文主要就是针对volatile
关键字做了简单的说明。
- volatile的运行原理。
- volatile与可见性、有序性和原子性问题的相关知识。
- 简单描述了计算机内存和缓存。
之后会再出文描述synchronized
关键字。