直接起飞。
今天只有残留的躯壳,迎接光辉岁月。
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内存屏障(内存栅栏)
屏障类型
|
指令示例
|
说明
|
LoadLoad
|
Load1; LoadLoad; Load2
|
保证load1的读取操作在load2及后续读取操作之前执行
|
StoreStore
|
Store1; StoreStore; Store2
|
在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
|
LoadStore
|
Load1; LoadStore; Store2
|
在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
|
StoreLoad
|
Store1; StoreLoad; Load2
|
保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行
|
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。
状态
|
描述
|
监听任务
|
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~
你指尖跃动的电光,是我此生不变的信仰,唯我超电磁炮永世长存!