并发编程(3):volatile关键字原理、happens-before模型

1、volatile关键字的作用就是解决了线程间的可见性问题。

2、什么是线程可见性问题?

       在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程 环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最 新的值。这就是所谓的可见性。

3、volatile关键字的基本用法

   public class VolatileDemo1 implements Runnable{

    public volatile static boolean stop = false;

    @Override
    public void run() {
        int i = 0;
        while (!stop){
            i++;

            /*
            方式1:释放锁操 会强制工作内存的写操作的数据刷新到主内存。
                   System.out.println("2132");   由于输出的方法里面存在释放锁操作。
                   synchronized (this){
                   }
             */
            //System.out.println("2132");
            //synchronized (this){
            //}


            /*
            方式2:IO操作 会强制工作内存的写操作的数据刷新到主内存。
                   new File("c:/test.txt");
             */
            //new File("c:/test.txt");


            /*
            方式3:线程沉睡 会强制工作内存的写操作的数据刷新到主内存。
                   Thread.sleep(long);
             */
            //Thread.sleep(0);
        }
        System.out.println("rs:" + i);
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new VolatileDemo1(),"t1").start();
        Thread.sleep(1000);
        stop=true;
        /*
        当stop变量不使用volatile 修饰的时候,主线程修改了其值以后,
        对于t1线程来说是不可见的,所以会存在可见性问题。

        当我们使用volatile来修饰stop变量变量的时候,一旦主线程修改了
        stop变量的值,对于他线程来说就是可见的。

        除了使用volatile来修饰stop 变量,我们还有其他方式,我们可以执行synchronized操作,因为
        锁释放的操作会强制将工作内存的写操作的数据刷新到主内存。
        除了释放锁操作可以刷新主内存之外,还有IO操作也会强制刷新主内存,如 new File("c:/test.txt")。
        除此之外 Thread.sleep 也会强制将工作内存的写操作的数据刷新到主内存。
         */
    }
}

4、volatile加上以后对Java代码编译成汇编指令后有啥区别?

     我们使用hsdis工具来查看Java 代码通过编译后的汇编指令。
     工具用法:1、将如下图中的两个文件添加到JRE_HOME/bin/server路径下。

                    
                      2、在运行main函数之前,加入如下虚拟机参数:
                          -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

                          -XX:CompileCommand=compileonly,*VolatileDemo1.getInstance(替换成实际运行的代码)
                   
       我们使用工具可发现,使用volatile关键字之后,Java代码编译成的汇编多了一个Lock指令。
       0x00000000037028f3: lock add dword ptr [rsp],0h  ;*putstatic stop

5、我们上面说了加上volatile关键字在代码编译后的汇编指令会多一个lock的指令,这个指令牵扯到cpu缓存锁的概念,下面我们来探究一下

          在计算机中cpu层面有缓存的概念,叫做cpu的高速缓存,如下视图:

            

              cpu在执行指令的时候,会先去高速缓存中查找,比如定义一个变量static boolean stop = false,如果cpu执行的线程需要使用此变量,会先去cpu的缓存中找,如果没有就去主内存中找,找到后就会放到cpu缓存中,方便下一次获取使用,读写都是操作cpu的缓存。如果多个线程由多个cpu去执行的时候都使用到了stop变量,那么就会有多个cpu缓存了stop变量,这个时候如果有线程修改了stop变量(修改的是当前cpu中的缓存,会不确定时间同步主内存。),且还没有同步到主内存,那么其他线程使用stop变量的时候就出现了值不是最新的,这也就是可见性问题产生的原因。这个时候就引出了cpu缓存一致性的问题。毫无疑问为了处理多cpu的缓存一致性问题,操作系统的开发人员允许使用加锁来解决cpu缓存一致性的问题,如上图所示cpu与内存交互是通过总线的,那么cpu就提供了一个 总线锁,总线锁会阻塞其他cpu,开销比较大,因此操作系统开发人员由提供了 缓存锁,也就是收保护的资源是cpu的高速缓存。前面说的lock指令就跟这两个锁有关系。

          那什么时候使用缓存锁呢?
              1、cpu架构支持缓存锁。
              2、当前数据是否存在于缓存。

         上面抛出了多cpu缓存一致性问题,在解决这个问题的方案中由很多的缓存一致性协议如msi、mesi、mosi等协议。
          我们主要说明一下mesi协议,mesi表示4钟缓存状态,如下:
              1. M(Modify) 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不致。
              2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改 
              3. S(Shared) 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致 
              4. I(Invalid) 表示缓存已经失效

              这种一致性协议的作用如下图:

                        

           
             早期cpu缓存过期通知策略:

                  

             这种方式会造成cpu阻塞,开销较大,为了解决这个问题,操作系统的开发人员引入了Store Bufferes的概念,如下图:

                  

                  引入Store Bufferes概念后会导致一个严重的问题产生,那就是指令重排序,比如下代码运行结果可能会出现错误。

           executeToCPU0(){   
               a=1;   
               b=1; 
           } 

           executeToCPU1(){    
               while(b==1){        
                  assert(a==1);  
               } 
           }

                  假设分别有两个线程,分别执行executeToCPU0和executeToCPU1,分别 由两个不同的CPU来执行。 引入Store Bufferes之后,就可能出现 b==1返回true ,但是assert(a==1)返回false。原因是cpu0在执行a=1的时候,会先将指令放入到Store Bufferes中,然后异步通知cpu1先失效,而此时cpu0可能会出现先将b=1的指令执行完成了才正在的从Store Bufferes中获取
到a=1的指令执行,这就是Store Bufferes引起cpu指令重排序的问题。 如下流程:

                 为了解决指令重排序问题,操作系统的开发人员引入了 内存屏障 的模型。

                     通过内存屏障禁止指令重排:

                       X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)。

                       1、Store Memory Barrier(写屏障):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的
                              数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后
                              的读或者写是可见的,可理解为立马同步主内存
                       2、Load Memory Barrier(读屏障):处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏
                             障之前的内存更新对于读屏障之后的读操作是可见的,可理解为直接读取主内存。
                       3、Full Memory Barrier(全屏障):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。

                       伪代码如下: 

                    volatile int a=0;  //volatile关键字添加的lock指令就是在指令的前后加上了读写的内存屏障来保证线程的可见性。
                    executeToCpu0(){ 
                         a=1;    
                         storeMemoryBarrier() //写屏障,会立马将a=1写入到内存,
                                              //屏障后的读取都是最新的,插入这个
                                              //屏障也就表明a=1; b=1;两个指令不允许指令重排序。
                         b=1;
                    } 
   
                    executeToCpu1(){ 
                         loadMemoryBarrier(); //读屏障,可理解为屏障后的指令,直接去住内存读取数据。
                         assert(a==1) //true    
                    }

                        不同的cpu架构、os 架构的内存屏障指令可能不同,因为Java 是一次编写,到处运行,所以Java中就提供了一个Java内存模型(JMM)来处理因为平台差异化内存访问方式不同的问题。JMM本身是一种抽象的概念,并不真实存在,它定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。JMM 屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能使用Java提供的高级指令(如volatile)来达到一致的内存访问效果。

                  

 

6、有了上面内存屏障的知识点,我们回归volatile关键字的原理

     我们查看Java编译后的class字节码发现volatile修饰的变量会多一个ACC_VOLATILE 的class指令(注意区别于汇编的lock)。
      public static volatile boolean stop;    
         descriptor: Z    
         flags: ACC_PUBLIC, ACC_STATIC,
ACC_VOLATILE

       这个class 指令就是在jvm对stop 指令进行读写的时候,会判断变量是否有ACC_VOLATILE指令,如果有就会在赋值后加上内存屏障的操作。

         如下jvm源码(bytecodeInterpreter.cpp):

 int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            OrderAccess::storeload();
          } else {
            if (tos_type == itos) {
              obj->int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->double_field_put(field_offset, STACK_DOUBLE(-1));
            }
          }

                代码中的OrderAccess::storeload();表示添加写屏障,我们查看其jvm源码如下:

                OrderAccess::storeload();这个操作在jvm中就有不同os架构下的是实现,如下:

               

                   我们挑选x86的实现看其源码如下: 

inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}

inline void OrderAccess::release() {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

                   inline void OrderAccess::storeload()  { fence(); }  方法的部分实现如下:

                   __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); 看见没有lock指令,这就是之前说的Java代码编译成的汇编volatile关键字多出来的lock指令,volatile 修饰的变量,就是在读写的时候,jvm里面自动为其加上内存屏障来解决可见性。

 

7、happens-before 模型

     除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。 

     7.1、程序顺序规则(as-if-serial语义):

              规定1、不能改变程序的执行结果(在单线程环境下,执行的结果不变)。

              规定2、依赖问题, 如果两个指令存在依赖关系,是不允许重排序。

              案例:

                 int a=0; 
                 int b=0; 
                 void test(){    
                    int a=1;    a    
                    int b=1;    b    

                    //允许a、b赋值重排序。
                    //int b=1;    
                    //int a=1;    
                    int c=a*b;  c 
                 }

                a happens -before b  ;  b happens before c      b happens before c  的意思就是b对于c出的操作是可见的。

 

     7.2、传递性规则 ​​​:

                如果 a happens-before b  , b happens- before c,   那么 a happens-before c 

 

     7.3、volatile变量规则

                volatile 修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作.,其实现为内存屏障机制来防止指令                  重排。

                案例 : 

                 public class VolatileExample{
                     int a=0;    
                     volatile boolean flag=false; 
                     public void writer(){        
                           a=1;                         1        
                           flag=true; //修改            2    
                     } 

                     public void reader(){         
                           if(flag){ //true             3            
                               int i=a;//1              4        
                           }    
                     } 
                }
 

 

               1 happens-before 2 是否成立?  是 
               3 happens-before 4  是否成立?  是 

               2 happens -before 3  ->volatile规则
               1 happens-before 4 ;     i=1成立。

               volatile规则里面有一个规则如下表: 
                根据这个表可知道案例中 1 happens-before 2

 

     7.4、监视器锁规则:

                  案例:

             int x=10; 
             synchronized(this){    
                   //后续线程读取到的x的值一定12    
                   if(x<12){         
                      x=12;    
                   } 
             }
 
             其他线程在此处读取到的值一定是x=12
             x=12;


     7.5、start规则:

                 案例:

                public class StartDemo{    
                    int x=0;    
                    Thread t1=new Thread(()->{        
                          //读取x的值  一定是20        
                          if(x==20){                   
                          }    
                    });        
 
                    //启动线程之前先设置为x=20, 那么在t1里获取的x一定等于20.
                    x=20;    
                    t1.start();        
                }

         

     7.6、join规则:         

                   案例:

                   public class Test{   
                      int x=0;    
                      Thread t1=new Thread(()->{      
                          x=200;    
                      }); 
  
                      t1.start();  
                      t1.join(); //保证结果的可见性。    
                      在此处读取到的x的值一定是200。
                   } 

      7.7、final关键字也提供了内存屏障的规则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值