JAVA并发编程(十一)之volatile

一、并发编程的三要素

原子性 :原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

可见性 :可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他 线程可以立即看到修改的结果。

有序性 :有序性,即程序的执行顺序按照代码的先后顺序来执行。

为了使程序正确执行,我们需要满足以上三个条件,但是在并发环境中,往往是会产生冲突的。

1、JAVA初始化对象字节码执行乱序问题(this溢出问题)

我们来看new对象的执行步骤:

  • new ThreadThisTest() :为对象申请空间,并将成员变量mk初始化化为0;
  • dup:压栈操作
  • invokespecial:调用构造函数初始化对象,将成员变量mk设置为10;
  • astore_1:建立关联,将栈中的变量thisTest指向new 出来的对象;

如果invokespecial与astore_1指令执行顺序改变,我们的代码执行的结果就会是0;由此问题我们就知道为什么在double check 单例模式中我们为何要加上volatile关键字了

二、volatile

volatile的两大作用:

  1. 保证共享变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,并且每次使用都会从主内存获取。
  2. 禁止指令重排序优化,有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),单核CPU无需内存屏障;

汇编指令lock的作用:

  • 重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 使得本CPU的Cache写入内存
  • 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见

volatile指令的有序性:为了禁止指令的重排序,编译器会在编译指令的时候,插入内存屏障来保证volatile指令的有序性:

  1. 对于写操作:在每个volatile写操作的前面插入一个StoreStore屏障,再在其后面插入一个StoreLoad屏障。
  2. 对于读操作:在每个volatile读操作的后面插入一个LoadLoad屏障,再在其后面插入一个LoadStore屏障。
  • StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
  • StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
  • LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
  • LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

double-check singleton Pattern:

public class DoubleSingle {
    //volatile 作用防止创建对象时指令的重排序
   private static volatile DoubleSingle doubleSingle;
   private DoubleSingle(){

   }
   public static DoubleSingle getInstance(){
       //第一层判断,如果不为空直接返回
       if(doubleSingle==null){
           //加锁防止多线程同时来创建对象
           synchronized (DoubleSingle.class){
               //再判断是防止多个线程一起竞争锁,当某个线程创建对象后,
               // 其他获得锁的线程进入代码后做再次检查,避免重复创建
               if(doubleSingle==null){
                   doubleSingle=new DoubleSingle();
               }
           }
       }
       return doubleSingle;
   }
}

加volatile的原因:

对象初始化的逻辑:如果2和3重排序,那么single指向的是一个未初始化的对象

  1. single = allocate();  // 为对象开辟内存空间
  2. ctorInstance(DoubleSingle); // 2:初始化对象
  3. instance = single;  // 3:设置instance指向刚分配的内存地址

volatile不能保证原子性:例如i++
volatile的integer自增(i++),其实要分成3步:

  • 1)读取volatile变量值到local; 
  • 2)增加变量的值;
  • 3)把local的值写回,让其它的线程可见。

这3步的jvm指令为:

  1. mov    0xc(%r10),%r8d ; Load
  2. inc    %r8d           ; Increment
  3. mov    %r8d,0xc(%r10) ; Store
  4. lock addl $0x0,(%rsp) ; StoreLoad Barrier

例如:线程A先读取i,然后A被阻塞了,这时线程B也去读取变量i,由于线程A只是读取,没有做修改操作,所以线程B读取的数据与A相同都是0,线程B继续执行i=i+1=1,并把1写入工作内存,最后写入主存;然后线程A获取到了CPU资源,执行i=i+1=1;并把1写入工作内存,最后写入主存;由此可见对于非原子操作voalitle是无法保证其执行结果的。
使用volatile必须具备以下2个条件:

  1. 对变量的写操作不依赖于当前值 
  2. 该变量没有包含在具有其他变量的不变式中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值