光輝歲月

导航

 

  直接起飞。

  今天只有残留的躯壳,迎接光辉岁月。

 

  1.并发编程的三大问题?

  原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不 会被其他线程影响(对于原子性的代码块或者原子性变量,多线程环境下面是隔离的,互不影响的;或者说是代码是同步互斥执行)(32位系统中,对于long和double 64位存储的类型,很有可能一个线程完成前面32位操作的时候,另一个线程读到了剩下的32位;比较少发生,知道32位系统有这个问题就行)。

  可见性:可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值(临界资源更改其他线程能够及时感知)。

  有序性:有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这 样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序 现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺 序未必一致(JIT编译器下【jvm默认的编译模式是混合模式,JIT是默认开启的】,指令码不一定是有序的,如果指令码重排了,程序就不一定是从上往下执行了)。

 

  2.volatile解决了哪些问题?

  

  2.1volatile解决可见性问题

  我们先来看下面的代码:

public class VolatileTest {

    private boolean initFlag = false;


    public void save(){
         this.initFlag = true;
         String threadname = Thread.currentThread().getName();
         System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }


    public void load(){
        String threadname = Thread.currentThread().getName();
        while (!initFlag){
             //线程在此处空跑,等待initFlag状态改变
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
    }

    public static void main(String[] args){
        VolatileTest sample = new VolatileTest();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                sample.save();
            }
        }, "threadA");

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                sample.load();
            }
        },"threadB");

        threadB.start();
        try {
          Thread.sleep(1000);
           } catch (InterruptedException e) {
            e.printStackTrace();
            }
        threadA.start();
    }

}

  运行结果:

 

 

   线程B,并没有感知到 initFlag 的更改,一直在跑空循环。我们把变量initFlag用volatile修饰,看看:

public class VolatileTest {

    private volatile boolean initFlag = false;


    public void save(){
         this.initFlag = true;
         String threadname = Thread.currentThread().getName();
         System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }


    public void load(){
        String threadname = Thread.currentThread().getName();
        while (!initFlag){
             //线程在此处空跑,等待initFlag状态改变
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
    }

    public static void main(String[] args){
        VolatileTest sample = new VolatileTest();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                sample.save();
            }
        }, "threadA");

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                sample.load();
            }
        },"threadB");

        threadB.start();
        try {
          Thread.sleep(1000);
           } catch (InterruptedException e) {
            e.printStackTrace();
            }
        threadA.start();
    }

}

  运行结果:

 

 

   这次B线程及时嗅到了initFlag的更改。

  

  2.2volatile解决指令重排问题

  我们来看一个经典的单例模式代码:

public class SingletonMod {

    private static SingletonMod instance;

    private SingletonMod(){}

    public static SingletonMod getInstance(){

        if (instance == null) {
            synchronized (SingletonMod.class) {
                if (instance == null){
                    instance = new SingletonMod();
                }
            }
        }
        return instance;
    }
}

  这是一个单例模式的代码,不从并发角度来分析,看上去是没有什么问题的。但是,我们来看指令码吧:

 

 

 在指令码上面,instance = new SingletonMod(); 被拆解成3个指令了,这3个指令代表什么意思可以去查看指令码手册,但是,这3个指令重排的话,那就问题大了。

  我们可以这样理解:

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

  上述指令如果发生重排的话,很有可能是下面这种执行顺序:

memory =allocate();    //1:分配对象的内存空间 
instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);  //2:初始化对象

  所以说在并发的情况下面,这个单例模式,很有可能返回的是个null。

 

  如果给instance加上volatile关键字,那么就不存在对象初始化指令的重排现象。代码如下所示:

  

public class SingletonMod {

    private volatile static SingletonMod instance;

    private SingletonMod(){}

    public static SingletonMod getInstance(){

        if (instance == null) {
            synchronized (SingletonMod.class) {
                if (instance == null){
                    instance = new SingletonMod();
                }
            }
        }
        return instance;
    }
}

  这才是一个完整的,线程安全的单例模式。

  2.3volatile并不能解决原子性问题

  我们看下面代码:

public class AtomicityTest {

    public static volatile int i = 0;

    public static void add() {
        i++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j< 1000; j++) {
                        add();
                    }
                }
            }).start();
        }

        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(i);
    }
}

  我们看看执行结果,如果代码能保证原子性的话,那么结果就应该是10000;

  结果如图,总是<=10000;

   虽然i被volatile修饰,但是i++并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线 程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程 一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败。(也可以从指令码上面来分析更能看出问题)

  volatile并不能解决原子性问题。

 

  3.volatile的有序性

  禁止指令重排序优化。

  3.1指令重排概念

  java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果 与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。(JIT指令优化器对指令码执行顺序的重新排序)

  3.2指令重排存在的必要性

  JVM能根据处理器特性(CPU多级缓存系统、多核处 理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

  3.3JAVA代码中存在指令重排?

  我们来看下面这段代码:

public class Instruction_Rearrangement {
    private  static int x = 0, y = 0;
    private  static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    shortWait(10000);
                    a = 1;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                log.info(result);
            }
        }

    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

  如果在代码中不存在重排现在,那么我们来罗列一下可能出现的x和y的值,看下面的时序图:

 

 

 

 

 

 

 

 

 

 

   上述六种情况,都不存在x=0和y=0的情况,那么程序应该是死循环,不会被终止。我们来运行一下,看看结果:

   我们可以看到,在运行了27万多次,终于出现了x=0和y=0的情况,那么我们再来通关时序图来分析一下,为什么x=0和y=0会出现呢?

 

   我们可以看到,在平时程序中,确实是可能存在指令重排的情况。

 

  3.4内存屏障(内存栅栏)

  内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
  
  Intel硬件提供了一系列的内存屏障,主要有: 
    1. lfence,是一种Load Barrier 读屏障 
    2. sfence, 是一种Store Barrier 写屏障 
    3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力 
    4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
  不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:
  
屏障类型
指令示例
说明
LoadLoad
Load1; LoadLoad; Load2
保证load1的读取操作在load2及后续读取操作之前执行
StoreStore
Store1; StoreStore; Store2
在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore
Load1; LoadStore; Store2
在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad
Store1; StoreLoad; Load2
保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

  

  由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。(volatile原理上是程序调用系统级别的硬件内存屏障,在计算机能够识别的指令上面加上内存屏障,防止cpu对操作指令重排)
 

  3.5JVM对volatile的重排规则

第一个操作
第二个操作:普通读写
第二个操作:volatile读
第二个操作:volatile写
普通读写
可以重排
可以重排
不可以重排
volatile读
不可以重排
不可以重排
不可以重排
volatile写
可以重排
不可以重排
不可以重排

  总结上面的表格,我们可以得出下面结果:

    1:当第一个操作是普通读写时,第二个操作只有是volatile写才不会被排序。

    2:当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

    3:当第一个操作是volatile写时,第二个操作只有是普通读写的时候才能被重新排序。

    4:当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

  上述这些重排规则,第一个操作和第二个操作都是相对的,并不是绝对的。(第一个操作如果上面有一行代码,那么对于上面的一行代码来说,这个第一个操作就是第二个操作)

 

  3.6java指令与硬件内存屏障时序图解

  为了实现volatile的防止指令排序功能,编译器在生成字节码时,会在指令序列中插入内存屏障,来禁止特定类型的处理器重排序。

  

  对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

    1:在每个volatile写操作的前面插入一个StoreStore屏障。

    2:在每个volatile写操作的后面插入一个StoreLoad屏障。 

    3:在每个volatile读操作的后面插入一个LoadLoad屏障。 

    4:在每个volatile读操作的后面插入一个LoadStore屏障。

  上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到 正确的volatile内存语义。

 

  volatile写插入内存屏障后生成的指令时序图

 

   上图中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

  (注:volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile 写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile 写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即 return)。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个 volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效 率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad屏障。因为 volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一 个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad 屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率。)

 

  保守策略下的volatile读

   上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

   (上述都是因特尔处理器的内存屏障)

 

  注意了,这不是演习,我要装逼了,看我大招

  注意了,这不是演习,我要装逼了,看我大招

  注意了,这不是演习,我要装逼了,看我大招

 

  上面的指令优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。(不同处理器的指令排序优化不同)

  下面以x86处理器为例:

    X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排 序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着 在 X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障 开销会比较大)。

    前面保守策略下的volatile读和写,在X86处理器平台可以优化成如下图所示:

 

 

  4.JAVA代码被CPU调度的过程

  我们手动敲的java代码,最终怎么被系统识别,怎么执行的呢?

  代码需要经过3次转行,最终才能被操作系统识别,CPU才能去调度程序,用下图来说的话:

  1:A.class经过类装载子系统。

  2:类装载子系统把A.class相关字节码装载到原空间

  3:程序调用a对象,字节码执行引擎执行A.class的指令码

  4:JIT指令码优化器优化jvm指令,并且转为汇编原语(能够调度硬件的底层语言)

  5:汇编原语解析成操作系统更够识别的010101二进制,并且调用硬件CPU,获取到CPU的线程执行权

  6:CPU执行java代码

  (上面6点和图,全文背诵,装逼必备)

 

  5.volatile可见性原理

  上述java代码执行过程中,硬件原语是很重要的一环,volatile就是在这里实现了变量副本的即时可见性。

  我们来看我们的java代码对应的汇编原语是什么,下面是没有加volatile的时候的汇编原语(怎么看的话,去idea装插件):

CompilerOracle: compileonly *VolatileTest.save
Loaded disassembler from hsdis-amd64.dll
Decoding compiled method 0x00000000037fd550:
Code:
Argument 0 is unknown.RIP: 0x37fd740 Code size: 0x00000718
[Disassembling for mach='amd64']
[Entry Point]
[Constants]
  # {method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest'
  #           [sp+0x50]  (sp of caller)
  0x00000000037fd740: mov     r10d,dword ptr [rdx+8h]
  0x00000000037fd744: shl     r10,3h
  0x00000000037fd748: cmp     r10,rax
  0x00000000037fd74b: jne     3735f60h          ;   {runtime_call}
  0x00000000037fd751: nop     word ptr [rax+rax+0h]
  0x00000000037fd75c: nop
[Verified Entry Point]
  0x00000000037fd760: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x00000000037fd767: push    rbp
  0x00000000037fd768: sub     rsp,40h
  0x00000000037fd76c: mov     rsi,17c84028h     ;   {metadata(method data for {method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x00000000037fd776: mov     edi,dword ptr [rsi+0dch]
  0x00000000037fd77c: add     edi,8h
  0x00000000037fd77f: mov     dword ptr [rsi+0dch],edi
  0x00000000037fd785: mov     rsi,17c82d78h     ;   {metadata({method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x00000000037fd78f: and     edi,0h
  0x00000000037fd792: cmp     edi,0h
  0x00000000037fd795: je      37fdc28h          ;*aload_0
                                                ; - com.ghsy.user.test.VolatileTest::save@0 (line 25)

  0x00000000037fd79b: mov     byte ptr [rdx+0ch],1h  ;*putfield initFlag
                                                ; - com.ghsy.user.test.VolatileTest::save@2 (line 25)

  0x00000000037fd79f: mov     rsi,17c84028h     ;   {metadata(method data for {method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x00000000037fd7a9: add     qword ptr [rsi+108h],1h
  0x00000000037fd7b1: nop
  0x00000000037fd7b2: nop
  0x00000000037fd7b3: nop
  0x00000000037fd7b4: nop
  0x00000000037fd7b5: nop
  0x00000000037fd7b6: nop
  0x00000000037fd7b7: call    3736620h          ; OopMap{off=124}
                                                ;*invokestatic currentThread
                                                ; - com.ghsy.user.test.VolatileTest::save@5 (line 26)
                                                ;   {static_call}
  0x00000000037fd7bc: cmp     rax,qword ptr [rax]  ; implicit exception: dispatches to 0x00000000037fdc3f
  0x00000000037fd7bf: mov     rdx,rax
  0x00000000037fd7c2: mov     rsi,17c84028h     ;   {metadata(method data for {method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x00000000037fd7cc: mov     edx,dword ptr [rdx+8h]
  0x00000000037fd7cf: shl     rdx,3h
  0x00000000037fd7d3: cmp     rdx,qword ptr [rsi+120h]
  0x00000000037fd7da: jne     37fd7e9h
  0x00000000037fd7dc: add     qword ptr [rsi+128h],1h
  0x00000000037fd7e4: jmp     37fd84fh
  0x00000000037fd7e9: cmp     rdx,qword ptr [rsi+130h]
  0x00000000037fd7f0: jne     37fd7ffh
  0x00000000037fd7f2: add     qword ptr [rsi+138h],1h
  0x00000000037fd7fa: jmp     37fd84fh
  0x00000000037fd7ff: cmp     qword ptr [rsi+120h],0h
  0x00000000037fd80a: jne     37fd823h
  0x00000000037fd80c: mov     qword ptr [rsi+120h],rdx
  0x00000000037fd813: mov     qword ptr [rsi+128h],1h
  0x00000000037fd81e: jmp     37fd84fh
  0x00000000037fd823: cmp     qword ptr [rsi+130h],0h
  0x00000000037fd82e: jne     37fd847h
  0x00000000037fd830: mov     qword ptr [rsi+130h],rdx
  0x00000000037fd837: mov     qword ptr [rsi+138h],1h
  0x00000000037fd842: jmp     37fd84fh
  0x00000000037fd847: add     qword ptr [rsi+118h],1h
  0x00000000037fd84f: mov     rdx,rax           ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)

  0x00000000037fd852: nop
  0x00000000037fd853: nop
  0x00000000037fd854: nop
  0x00000000037fd855: nop
  0x00000000037fd856: nop
  0x00000000037fd857: call    37361a0h          ; OopMap{off=284}
                                                ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)
                                                ;   {optimized virtual_call}
  0x00000000037fd85c: nop     dword ptr [rax+0h]
  0x00000000037fd860: jmp     37fdca1h          ;   {no_reloc}
  0x00000000037fd865: add     byte ptr [rax],al
  0x00000000037fd867: add     byte ptr [rax],al
  0x00000000037fd869: add     byte ptr [rsi+0fh],ah
  0x00000000037fd86c: Fatal error: Disassembling failed with error code: 15Decoding compiled method 0x0000000003800e10:
Code:
Argument 0 is unknown.RIP: 0x3800f60 Code size: 0x00000088
[Entry Point]
[Constants]
  # {method} {0x0000000017c82d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest'
  #           [sp+0x20]  (sp of caller)
  0x0000000003800f60: mov     r10d,dword ptr [rdx+8h]
  0x0000000003800f64: shl     r10,3h
  0x0000000003800f68: cmp     rax,r10
  0x0000000003800f6b: jne     3735f60h          ;   {runtime_call}
  0x0000000003800f71: nop
  0x0000000003800f74: nop     dword ptr [rax+rax+0h]
  0x0000000003800f7c: nop
[Verified Entry Point]
  0x0000000003800f80: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000003800f87: push    rbp
  0x0000000003800f88: sub     rsp,10h
  0x0000000003800f8c: mov     byte ptr [rdx+0ch],1h  ;*synchronization entry
                                                ; - com.ghsy.user.test.VolatileTest::save@-1 (line 25)

  0x0000000003800f90: mov     rdx,qword ptr [r15+1d0h]
  0x0000000003800f97: call    37361a0h          ; OopMap{off=60}
                                                ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)
                                                ;   {optimized virtual_call}
  0x0000000003800f9c: mov     edx,43h
  0x0000000003800fa1: mov     rbp,rax
  0x0000000003800fa4: nop
  0x0000000003800fa7: call    37357a0h          ; OopMap{rbp=Oop off=76}
                                                ;*getstatic out
                                                ; - com.ghsy.user.test.VolatileTest::save@12 (line 27)
                                                ;   {runtime_call}
  0x0000000003800fac: int3                      ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)

  0x0000000003800fad: mov     rdx,rax
  0x0000000003800fb0: add     rsp,10h
  0x0000000003800fb4: pop     rbp
  0x0000000003800fb5: jmp     375fde0h          ;   {runtime_call}
  0x0000000003800fba: hlt
  0x0000000003800fbb: hlt
  0x0000000003800fbc: hlt
  0x0000000003800fbd: hlt
  0x0000000003800fbe: hlt
  0x0000000003800fbf: hlt
[Stub Code]
  0x0000000003800fc0: mov     rbx,0h            ;   {no_reloc}
  0x0000000003800fca: jmp     3800fcah          ;   {runtime_call}
[Exception Handler]
  0x0000000003800fcf: jmp     375f2a0h          ;   {runtime_call}
[Deopt Handler Code]
  0x0000000003800fd4: call    3800fd9h
  0x0000000003800fd9: sub     qword ptr [rsp],5h
  0x0000000003800fde: jmp     3737600h          ;   {runtime_call}
  0x0000000003800fe3: hlt
  0x0000000003800fe4: hlt
  0x0000000003800fe5: hlt
  0x0000000003800fe6: hlt
  0x0000000003800fe7: hlt
线程:threadA:修改共享变量initFlag
线程:threadB当前线程嗅探到initFlag的状态的改变

  上面的这是VolatileTest.java的sava方法的汇编原语,我们再来看给initFlag加上volatile修饰以后的汇编原语:

CompilerOracle: compileonly *VolatileTest.save
Loaded disassembler from hsdis-amd64.dll
Decoding compiled method 0x0000000002d5d590:
Code:
Argument 0 is unknown.RIP: 0x2d5d780 Code size: 0x00000718
[Disassembling for mach='amd64']
[Entry Point]
[Constants]
  # {method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest'
  #           [sp+0x50]  (sp of caller)
  0x0000000002d5d780: mov     r10d,dword ptr [rdx+8h]
  0x0000000002d5d784: shl     r10,3h
  0x0000000002d5d788: cmp     r10,rax
  0x0000000002d5d78b: jne     2c95f60h          ;   {runtime_call}
  0x0000000002d5d791: nop     word ptr [rax+rax+0h]
  0x0000000002d5d79c: nop
[Verified Entry Point]
  0x0000000002d5d7a0: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000002d5d7a7: push    rbp
  0x0000000002d5d7a8: sub     rsp,40h
  0x0000000002d5d7ac: mov     rsi,171e4028h     ;   {metadata(method data for {method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x0000000002d5d7b6: mov     edi,dword ptr [rsi+0dch]
  0x0000000002d5d7bc: add     edi,8h
  0x0000000002d5d7bf: mov     dword ptr [rsi+0dch],edi
  0x0000000002d5d7c5: mov     rsi,171e2d78h     ;   {metadata({method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x0000000002d5d7cf: and     edi,0h
  0x0000000002d5d7d2: cmp     edi,0h
  0x0000000002d5d7d5: je      2d5dc70h          ;*aload_0
                                                ; - com.ghsy.user.test.VolatileTest::save@0 (line 25)

  0x0000000002d5d7db: mov     esi,1h
  0x0000000002d5d7e0: mov     byte ptr [rdx+0ch],sil
  0x0000000002d5d7e4:    add dword ptr [rsp],0h  ;*putfield initFlag
                                                ; - com.ghsy.user.test.VolatileTest::save@2 (line 25)

  0x0000000002d5d7e9: mov     rsi,171e4028h     ;   {metadata(method data for {method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x0000000002d5d7f3: add     qword ptr [rsi+108h],1h
  0x0000000002d5d7fb: nop
  0x0000000002d5d7fc: nop
  0x0000000002d5d7fd: nop
  0x0000000002d5d7fe: nop
  0x0000000002d5d7ff: call    2c96620h          ; OopMap{off=132}
                                                ;*invokestatic currentThread
                                                ; - com.ghsy.user.test.VolatileTest::save@5 (line 26)
                                                ;   {static_call}
  0x0000000002d5d804: cmp     rax,qword ptr [rax]  ; implicit exception: dispatches to 0x0000000002d5dc87
  0x0000000002d5d807: mov     rdx,rax
  0x0000000002d5d80a: mov     rsi,171e4028h     ;   {metadata(method data for {method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest')}
  0x0000000002d5d814: mov     edx,dword ptr [rdx+8h]
  0x0000000002d5d817: shl     rdx,3h
  0x0000000002d5d81b: cmp     rdx,qword ptr [rsi+120h]
  0x0000000002d5d822: jne     2d5d831h
  0x0000000002d5d824: add     qword ptr [rsi+128h],1h
  0x0000000002d5d82c: jmp     2d5d897h
  0x0000000002d5d831: cmp     rdx,qword ptr [rsi+130h]
  0x0000000002d5d838: jne     2d5d847h
  0x0000000002d5d83a: add     qword ptr [rsi+138h],1h
  0x0000000002d5d842: jmp     2d5d897h
  0x0000000002d5d847: cmp     qword ptr [rsi+120h],0h
  0x0000000002d5d852: jne     2d5d86bh
  0x0000000002d5d854: mov     qword ptr [rsi+120h],rdx
  0x0000000002d5d85b: mov     qword ptr [rsi+128h],1h
  0x0000000002d5d866: jmp     2d5d897h
  0x0000000002d5d86b: cmp     qword ptr [rsi+130h],0h
  0x0000000002d5d876: jne     2d5d88fh
  0x0000000002d5d878: mov     qword ptr [rsi+130h],rdx
  0x0000000002d5d87f: mov     qword ptr [rsi+138h],1h
  0x0000000002d5d88a: jmp     2d5d897h
  0x0000000002d5d88f: add     qword ptr [rsi+118h],1h
  0x0000000002d5d897: mov     rdx,rax           ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)

  0x0000000002d5d89a: nop
  0x0000000002d5d89b: nop
  0x0000000002d5d89c: nop
  0x0000000002d5d89d: nop
  0x0000000002d5d89e: nop
  0x0000000002d5d89f: call    2c961a0h          ; OopMap{off=292}
                                                ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)
                                                ;   {optimized virtual_call}
  0x0000000002d5d8a4: nop     dword ptr [rax+0h]
  0x0000000002d5d8a8: jmp     2d5dce9h          ;   {no_reloc}
  0x0000000002d5d8ad: add     byte ptr [rax],al
  0x0000000002d5d8af: add     byte ptr [rax],al
  0x0000000002d5d8b1: add     byte ptr [rsi+0fh],ah
  0x0000000002d5d8b4: Fatal error: Disassembling failed with error code: 15Decoding compiled method 0x0000000002d5d290:
Code:
Argument 0 is unknown.RIP: 0x2d5d3e0 Code size: 0x000000a8
[Entry Point]
[Constants]
  # {method} {0x00000000171e2d80} 'save' '()V' in 'com/ghsy/user/test/VolatileTest'
  #           [sp+0x20]  (sp of caller)
  0x0000000002d5d3e0: mov     r10d,dword ptr [rdx+8h]
  0x0000000002d5d3e4: shl     r10,3h
  0x0000000002d5d3e8: cmp     rax,r10
  0x0000000002d5d3eb: jne     2c95f60h          ;   {runtime_call}
  0x0000000002d5d3f1: nop
  0x0000000002d5d3f4: nop     dword ptr [rax+rax+0h]
  0x0000000002d5d3fc: nop
[Verified Entry Point]
  0x0000000002d5d400: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000002d5d407: push    rbp
  0x0000000002d5d408: sub     rsp,10h
  0x0000000002d5d40c: mov     byte ptr [rdx+0ch],1h
  0x0000000002d5d410: lock add dword ptr [rsp],0h  ;*putfield initFlag
                                                ; - com.ghsy.user.test.VolatileTest::save@2 (line 25)

  0x0000000002d5d415: mov     rdx,qword ptr [r15+1d0h]
                                                ;*invokestatic currentThread
                                                ; - com.ghsy.user.test.VolatileTest::save@5 (line 26)

  0x0000000002d5d41c: nop
  0x0000000002d5d41f: call    2c961a0h          ; OopMap{off=68}
                                                ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)
                                                ;   {optimized virtual_call}
  0x0000000002d5d424: mov     edx,43h
  0x0000000002d5d429: mov     rbp,rax
  0x0000000002d5d42c: nop
  0x0000000002d5d42f: call    2c957a0h          ; OopMap{rbp=Oop off=84}
                                                ;*getstatic out
                                                ; - com.ghsy.user.test.VolatileTest::save@12 (line 27)
                                                ;   {runtime_call}
  0x0000000002d5d434: int3                      ;*invokevirtual getName
                                                ; - com.ghsy.user.test.VolatileTest::save@8 (line 26)

  0x0000000002d5d435: mov     rdx,rax
  0x0000000002d5d438: add     rsp,10h
  0x0000000002d5d43c: pop     rbp
  0x0000000002d5d43d: jmp     2d53420h          ;   {runtime_call}
  0x0000000002d5d442: hlt
  0x0000000002d5d443: hlt
  0x0000000002d5d444: hlt
  0x0000000002d5d445: hlt
  0x0000000002d5d446: hlt
  0x0000000002d5d447: hlt
  0x0000000002d5d448: hlt
  0x0000000002d5d449: hlt
  0x0000000002d5d44a: hlt
  0x0000000002d5d44b: hlt
  0x0000000002d5d44c: hlt
  0x0000000002d5d44d: hlt
  0x0000000002d5d44e: hlt
  0x0000000002d5d44f: hlt
  0x0000000002d5d450: hlt
  0x0000000002d5d451: hlt
  0x0000000002d5d452: hlt
  0x0000000002d5d453: hlt
  0x0000000002d5d454: hlt
  0x0000000002d5d455: hlt
  0x0000000002d5d456: hlt
  0x0000000002d5d457: hlt
  0x0000000002d5d458: hlt
  0x0000000002d5d459: hlt
  0x0000000002d5d45a: hlt
  0x0000000002d5d45b: hlt
  0x0000000002d5d45c: hlt
  0x0000000002d5d45d: hlt
  0x0000000002d5d45e: hlt
  0x0000000002d5d45f: hlt
[Stub Code]
  0x0000000002d5d460: mov     rbx,0h            ;   {no_reloc}
  0x0000000002d5d46a: jmp     2d5d46ah          ;   {runtime_call}
[Exception Handler]
  0x0000000002d5d46f: jmp     2cbf2a0h          ;   {runtime_call}
[Deopt Handler Code]
  0x0000000002d5d474: call    2d5d479h
  0x0000000002d5d479: sub     qword ptr [rsp],5h
  0x0000000002d5d47e: jmp     2c97600h          ;   {runtime_call}
  0x0000000002d5d483: hlt
  0x0000000002d5d484: hlt
  0x0000000002d5d485: hlt
  0x0000000002d5d486: hlt
  0x0000000002d5d487: hlt
线程:threadA:修改共享变量initFlag
线程:threadB当前线程嗅探到initFlag的状态的改变

  上面是加了volatile的汇编原语,我们可以看出,initFlag前面加了一个lock前缀:

 

   下面是对应sava方法的第25行,我们来看代码:

 

   不就是25行,对应的initFlag的更改吗?

  我们继续来看这个lock前缀在汇编原语上面对应的解释(查看IA-32软件架构开发手册):

 

   我们可以看到,操作系统对于这个lock前缀做出的反应是加上了bus总线锁(早期CPU解决可见性方式,CPU的多核心效率完全没有发挥出来)(前面提过CPU访问内存条需要经过总线,如果总线加锁了,CPU是这个时候无法去访问内存条的),但是实际上,我们现在的CPU飞速发展,lock前缀现在会先去尝试缓存一致性协议(CPU的MESI协议)来做到在线程的工作空间中的变量副本保证一致性。

  6.CPU的缓存一致性协议(MESI)

  多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

  MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:
状态
描述
监听任务
M 修改 (Modified)
该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中
缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行
E 独享、互斥 (Exclusive)
该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中
缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态
S 共享 (Shared)
该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中
缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)
I 无效 (Invalid)
该Cache line无效

  【对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果 一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会 将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保 存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。 从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需 要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务】

   怎么来解释这个MESI协议呢?我们来看下面的图:

  1:线程0读取了主内存的变量x=0:

 

   2:线程1也读取了x=0到缓存里面,同时,线程0的副本状态由E改为S:

 

   3:线程0去更改x的值,改成x=2,线程0里面的副本状态改为M,但是线程1这个时候并未得知线程0做出了更改,状态还是S,值为x=0:

 

   4:线程0发出指令到bus总线,通知线程1,变量副本被我改了啊,你的这个副本失效了啊,没用了啊,把状态给我改成I:

 

   上述是简单情况下的MESi,如果是线程同时更改呢?

  线程同时更改的话,第四部的时候两个线程都会发指令到bus总线,通知对方副本失效,这个时候就需要总线做一个总线裁决,把副本更改全判断给某一个线程:

 

   5:假设总线裁决判给了线程0,那么线程1的第四部就不会通知到线程0,同时线程1的变量副本由M改为I状态

   主内存变量值已经更改,这个时候线程1去读取主内存(此时x=2),重复线程0的副本状态E,如果有其他线程读取,那么随之跟着更改,最终还是遵循上面的MESI状态。

 

 

  图解MESI状态转换:

  



 

 

触发事件
描述
本地读取(Local read)
本地cache读取本地cache数据
本地写入(Local write)
本地cache写入本地cache数据
远端读取(Remote read)
其他cache读取本地cache数据
远端写入(Remote write)
其他cache写入本地cache数据


 

 

  总结来说的话,看下面表格:

状态
触发本地读取
触发本地写入
触发远端读取
触发远端写入
M状态(修改)
本地cache:M 
触发cache:M
其他cache:I
本地cache:M 
触发cache:M
其他cache:I
本地cache:M→E→S
触发cache:I→S
其他cache:I→S
同步主内存后修改为E独享,同步触发、其他cache后本地、触发、其他cache修改为S共享
本地cache:M→E→S→I
触发cache:I→S→E→M
其他cache:I→S→I
同步和读取一样,同步完成后触发cache改为M,本地、其他cache改为I
E状态(独享)
本地cache:E
触发cache:E
其他cache:I
本地cache:E→M
触发cache:E→M
其他cache:I
本地cache变更为M,其他cache状态应当是I(无效)
本地cache:E→S
触发cache:I→S
其他cache:I→S
当其他cache要读取该数据时,其他、触发、本地cache都被设置为S(共享)
本地cache:E→S→I
触发cache:I→S→E→M
其他cache:I→S→I
当触发cache修改本地cache独享数据时时,将本地、触发、其他cache修改为S共享.然后触发cache修改为独享,其他、本地cache修改为I(无效),触发cache再修改为M
S状态(共享)
本地cache:S
触发cache:S
其他cache:S
本地cache:S→E→M
触发cache:S→E→M
其他cache:S→I 
当本地cache修改时,将本地cache修改为E,其他cache修改为I,然后再将本地cache为M状态
本地cache:S
触发cache:S
其他cache:S
本地cache:S→I
触发cache:S→E→M
其他cache:S→I
当触发cache要修改本地共享数据时,触发cache修改为E(独享),本地、其他cache修改为I(无效),触发cache再次修改为M(修改)
I状态(无效)
本地cache:I→S或者I→E
触发cache:I→S或者I →E
其他cache:E、M、I→S、I
本地、触发cache将从I无效修改为S共享或者E独享,其他cache将从E、M、I 变为S或者I
本地cache:I→S→E→M
触发cache:I→S→E→M
其他cache:M、E、S→S→I
既然是本cache是I,其他cache操作与它无关
既然是本cache是I,其他cache操作与它无关

 

 
M
E
S
I
M
×
×
×
E
×
×
×
S
×
×
I

 

 

  over~

  你指尖跃动的电光,是我此生不变的信仰,唯我超电磁炮永世长存!

 

posted on 2020-09-29 09:41  光輝歲月  阅读(486)  评论(0编辑  收藏  举报