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关键字最主要的作用就是:

  1. 保证变量的内存可见性。
  2. 局部阻止重排序的发生。

我们知道 synchronized关键字也能实现volatile关键字,但是为什么还需要volatile呢?

synchronized关键字

  1. 能保证可见性和原子性,但是其效率低下
  2. 多个线程同时只能有一个能够执行,线程之间存在互斥关系,有可能造成线程阻塞

volatile 关键字

  1. 看做一个轻量级的锁,但是又与锁有些不同;对于多线程,不是一种互斥关系 ,其运行效率更高
  2. 能够保证变量操作的有序性(禁止了指令重排)。

不保证原子性的解决方案

那么对于volatile关键字没有 原子性 有什么解决办法吗?

  1. 加锁的方式来保证原子性(synchronized)

  2. 原子变量(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 (singletonDemo
null){
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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值