JUC:浅析Volatile关键字
相关概念
在讲解Volatile关键字关键字之前,我们先来引入一些概念,有助于后续的理解。
JVM(java内存模型)规定:
对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其他线程的工作内存。
;如下图,如果线程要操作主内存中的共享变量,需要从主内存中拷贝一个副本到自己的工作内存中,然后进行对变量的操作,操作完毕之后再将变量刷新到主内存中。
在Java多线程并发过程中,需要具备三大性质才能保证线程的安全性,即:(内存)可见性、原子性、有序性。
而被volatile关键字修饰的变量使其具备了 可见性 和 有序性,但不能保证原子性。
那么什么是原子性、可见性、有序性呢?
可见性
当多个线程在操作主内存的某个变量时,其中有一个线程最先修改变量,并且将修改后的变量同步到了主内存时,会通知其他线程该变量已经被刷新,同时使他们所拷贝的变量副本无效,从而需要到主内存中重新读取该变量。这种通知其他线程主内存的变量已经被操作的方式,就是内存可见性。
如下例子,有助于理解:
// 变量i存放在主内存中
int i = 0; //语句1
// 线程1执行这条语句
i = 1; //语句2
//线程2执行的语句
j = i; //语句3
上面代码中,两个线程分别执行,两条线程都从主内存中拿走了变量i的副本,如果线程1先操作完成,并将变量同步到了主内存,(预期结果 j = 1)但是线程2并不知道其他线程对变量进行了操作,不知道主内存的变量i被修改了,继续执行其操作,最终得到结果 j = 0。
该例子出现的问题就是 内存不可见 所导致的。
而volatile关键字为我们提供了 可见性,就可以避免这种错误。
加了volatile关键字修饰后:
在线程1操作完成之后,主内存的变量i有了变化,随后立即通知其他线程其拷贝的副本作废,需要重新在主内存获取修改后的变量i。最终得到的结果为 j = 1 。
原子性
即具有原子性的操作,在执行的过程中要么全部执行,要么全部都不执行,并且在执行的过程中出现异常中断,那就全部不执行。原子性的操作不可被分割,其操作的过程中不能加塞其他操作。
下面是一个 不具备原子性 的例子:
int i = 0;
i++ ;//该操作不是原子性操作
//i++可分一下三个步骤
r1 = i; //从栈中取出i
r2 = r1 + 1; //i自增
i = r2 ; //将i存入栈中
面代码中,如果有2个线程都来执行i++,预期的结果本来应该是i最终为2;但是可能会出现这样的情况:首先两个线程都将变量i拷贝到自己的工作内存中,线程1当执行完前面两个步骤时,cpu突然将线程1挂起,此时线程1还没有将变量i写会到主内存中;线程2继续操作,当线程2已经完成操作并且将i写会到主内存后(i = 1),线程1又开始执行,这里注意:就算变量i有volatile关键字修饰,变量 i 具有了可见性,当主内存的变量被修改时会通知其他线程,但是由于线程1接下来的操作太快了,根本来不及发布通知,所有线程1也做了相同的操作(i = 1);两次操作只自增了一次,这是由于不具备原子性,而线程1在操作过程中加塞了其他操作(如挂起),从而导致丢失了写操作。
有序性
有序性即执行的顺序是按照代码先后的顺序来执行的。如下:
int i = 0;
boolean b = false;
i = 1; //语句1
b = true; //语句2
有序性就是按照 语句1、语句2 这样的顺序执行的,语句1在前,语句2在后。
而在Java内存模型中,允许编译器和处理器对指令进行重排序,即在不影响结果的情况下,可以不再按照先后顺序执行,语句2先执行便成为了可能。
例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
如果线程1的执行顺序进行了重排序,即先执行语句2再执行语句1,那么可能存在,当执行完语句2时,线程1也开始执行,执行到了最后发现context变量还未初始化,从而报错。
再看看下面一个例子:
new一个对象其实分三个步骤来完成,比如 String str = new String();可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
str = memory; //3.设置str指向刚分配的内存地址,此时str!= null;
由于指令重排的存在,再按照顺序执行与重排执行结果一样的现况下,是可以重排的;并且语句2和语句3不存在数据依赖的关系,那么他们的顺序可以交换:
memory = allocate(); //1.分配对象内存空间
str = memory; //3.设置str指向刚分配的内存地址,此时str!= null;但是对象还没有初始化完成!!
instance(memory); //2.初始化对象
如果在并发线程中执行这条语句,那么可能存在一条线程以第二种顺序的方式来执行,当执行完第二步时,此时str!= null,但是对象还没有初始化完成;那么其他线程可以来获取变量进行操作时,但是对象还没有初始化完成,所以造成了线程不安全的问题。
那么避免这种情况,volatile为我们提供了有序性,volatile屏蔽掉了JVM中必要的代码优化(指令重排序),因此在效率上比较低。
volatile关键字的作用
因此volatile关键字最主要的作用就是:
- 保证变量的内存可见性。
- 局部阻止重排序的发生。
我们知道 synchronized关键字也能实现volatile关键字,但是为什么还需要volatile呢?
synchronized关键字
- 能保证可见性和原子性,但是其效率低下;
- 多个线程同时只能有一个能够执行,线程之间存在互斥关系,有可能造成线程阻塞。
volatile 关键字
- 看做一个轻量级的锁,但是又与锁有些不同;对于多线程,不是一种互斥关系 ,其运行效率更高。
- 能够保证变量操作的有序性(禁止了指令重排)。
不保证原子性的解决方案
那么对于volatile关键字没有 原子性 有什么解决办法吗?
-
加锁的方式来保证原子性(synchronized)
-
原子变量(Atomic+数据类型),可以解决这个问题,在jdk1.5版本的java.util.concurrent.atomic包下提供了一些原子操作类,其内部是用CAS算法(Compare And Swap)来实现原子性操作的,具体原理请查看链接: CAS算法原理。
volatile的应用场景
单例模式
volatile关键字是对双重检索的单例模式的优化;我们先从简单的单例模式描述:
以下是一个懒汉模式的单例模式:
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
}
public static SingletonDemo getInstance(){
if (singletonDemo==null){
singletonDemo=new SingletonDemo();
}
return singletonDemo;
}
}
同样在单线程中使用这样的单例模式没什么问题,但是在多线性并发使用单例模式就会出现线程不安全的问题;如:首先singletonDemo为null还未被实例化,假如现在有两个线程AB需要使用单例,AB都将singletonDemo对象从主内存拷回自己的工作内存,AB都发现singletonDemo为null,因此他们都给singletonDemo进行了实例化,但是两个线程的实例对象并不是同一个,所以单例模式没有达到预期效果。
那么为了解决这个问题,我们用同步锁synchronized进行了优化:
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
}
public synchronized static SingletonDemo getInstance(){
if (singletonDemo==null){
singletonDemo=new SingletonDemo();
}
return singletonDemo;
}
}
双端检锁DCL机制
加了synchronized锁,保证了数据的一致性和可见性;但是在同一时间只能有一个线程才能调用这个方法,在singletonDemo!=null的情况下,其他线程本应该只是读取singletonDemo,但是现在所有的线程都有一个一个的等其他线程读取完了才能访问,这降低了并发的效率;因此,利用双端检锁DCL机制来优化:
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo(){
}
public static SingletonDemo getInstance(){
if (singletonDemonull){
synchronized (SingletonDemo.class){
if (singletonDemonull){
singletonDemo=new SingletonDemo();
}
}
}
return singletonDemo;
}
}
在锁前与锁后都进行检查一次,如果锁前singletonDemo=null就加锁,否则返回singletonDemo对象;加锁后再检查一次来保证数据有没有被修改过。但是!这样的DCL机制也不是线程安全的,因为singletonDemo=new SingletonDemo();这一步实例化操作是分为三步来进行的:(上面讲过)
memory=allocate();//1.分配对象内存空间
singletonDemo=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
singletonDemo(memory);//2.初始化对象
这三步操作可能会被指令重排序,那么可能会存在一个线程在创建单例模式的时候,以上面的顺序进行实例化,当完成第二行的时候,singletonDemo!=null了,那这个时候如果有其他线程要使用单例模式,在 第一个if (singletonDemo==null)的时候,不为null,那么 return singletonDemo;但是 singletonDemo还没有初始化完,那么会造成线程安全问题。
接下来就是volatile出场了,使用volatile关键字来禁止指令重排就可以解决了,只需要对singletonDemo加上修饰volatile即可:
private static volatile SingletonDemo singletonDemo=null;
CAS思想的底层实现
直接看源码,在compareAndSwapInt方法中,我们需要this和valueOffset来获取对象此时真正的值,那么this应该就是具有可见性的,需要用volatile修饰,this就是上面用volatile修饰的value。