x86内存变量可以在寄存器中,write buffer中,L1到L3cache中,主存中。寄存器、writebuffer和L1cache或者L2cache是cpu私有的。其中对程序员可编程的是寄存器和主存。cpu如何将变量写到writebuffer和如何写到cache对程序员是透明的。一般cpu读写内存的流程如下:
1.将地址发送到适当的cache中。
2.如果cache发出命中信号,请求的字就会出现在数据线上。
3.如果cache发出缺失信号,则把地址送到主存。当主存返回数据时,把它写入cache后再读出以满足请求。
所以无论任何情况cpu都不会绕过缓存,必须先去读缓存,然后再根据是否发生miss再确定是否读主存。
1.将地址发送到适当的cache中
2.如果cache发出命中信号,则写入成功。
3.如果cache发出缺失信号,如果当前cache行的数据被标记为W(也就是重写过)则将当前cache行的数据写回指定主存地址,然后从主存将当前地址的数据取到缓存行,最后进行写入 (当前内存地址不在缓存行)。
写cache一般分两个周期,第一个周期检查是否命中,第二个周期执行写入操作。或者使用write buffer来保存数据——通过流水线可以使写入操作只花费一个周期,如果使用write buffer,处理器在正常的cache访问周期内查找cache并把数据存储到write buffer中,如果cache命中,则在下一个还未用到的cache访问周期,将数据从write buffer写入到cache。
我们可以通过cache一致性保证从cache中获取到的数据是最新的,现代CPU已经不会强制读写主存了,cpu也没有对应的强制读写主存的指令。mfnece和lock指令只是防止重排序(并且lock有锁定缓存行的功能)没有强制读写主存的功能。intel手册中说,对于Intel486和Pentium处理器,LOCK#信号在LOCK操作期间始终在总线上置位,即使被锁定的存储器区域缓存在处理器中也是如此,也就是486和奔腾处理器使用LOCK信号会强制读写内存。但是对于P6和更新的处理器系列,如果在LOCK操作期间被锁定的存储器区域在cache中并且cache使用写回机制,则处理器不会断言 总线上的LOCK#信号。除非发生了cache miss。 相反,它将在缓存中修改变量值,缓存一致性机制可以确保操作以原子方式执行。 此操作称为“缓存锁定”。缓存一致性机制自动防止缓存相同内存区域的两个或多个处理器同时修改该区域中的数据。所以在P6(1995年)以后的处理器已经不会强制读写内存了,都是先去读写cache,通过cache一致性机制实现原子的读写。LOCK如果没发生cache miss那么锁定的就是缓存行。
在c语言中volatile只有一种语义,就是防止编译器将变量缓冲到寄存器,在多线程或者IO寄存器映射到内存的情况下,如果变量被缓冲到了通用寄存器会导致程序出错。1.在多线程情况,由于寄存器是私有的,如果两个线程被分配到了不同的cpu执行,此时全局变量被编译器缓存到了cpu寄存器,读写都会写进寄存器,这样会导致在其它cpu运行的线程看不到变量的最新值,当然这个也和编译器的优化级别有关,并不是一定会缓冲到cpu寄存器。2.在IO寄存器下如果对变量的修改不能同步到IO寄存器内将会出现更大问题。
所以C语言的volatile只能控制到编译器级别,使编译器不把变量缓冲到通用寄存器。
java定义了一个抽象的JMM,java内存模型,以适用于不同的平台。JMM中有一个happens-before顺序,两个动作可以按照happens-before关系进行排序, 也就是如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前是可见的。如果我们有两个动作x和y,我们可以用hb(x,y)来表示x发生在y之前,则有以下几种情况:
1.如果x和y是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y)也就是x对y可见。
2.如果x解锁,y加锁,则x在y之前执行。
3.如果x线程往一个volatile变量写,随后y线程读这个volatile变量,则x写的变量值对y可见。
4.如果线程x调用Thread.start()启动y线程,则x线程的start的操作要在线程y任意操作之前。
5.如果线程x调用Thread.join()等待y线程执行完,则y线程的任意操作都在x线程Thread.join()之前。
6.如果x在y之前执行hb(x,y), y在z之前执行hb(y,z),则x在z之前执行hb(x,z)。
可以看到java中volatile遵守happens-before规则,如果一个线程对共享变量做了更改,另外一个线程可以立即看到更新值。这个规则其实是jit或者java解释器做的,我们可以把jit或者解释器当作一个GCC编译器,GCC是将C/C++转换成二进制,JIT/java解释器是将字节码转换成二进制。也就是说这个happens-before规则只在编译器级别,具体cpu执行还是得看cpu的内存模型。
光看理论不行,需要看看jvm怎么处理volatile变量的,不管什么语言最终是要变为二进制代码执行的,所以必须要看java程序对应的汇编语言,可以使用hsdis将字节码转换为汇编语言,下面写一个简单的程序看看对应的汇编代码:
public class TestVolatile{
public static int value;
public static void main(String[] args) {
int a = 10;
value = 9;
value += a;
}
}
程序就是定义一个static int变量,在main方法中将变量初始化为9,然后再将变量加10。使用以下命令
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp TestVolatile > TestVolatile.asm
可以生成汇编代码,下面看对应的汇编代码:
Argument 0 is unknown.RIP: 0x7fdf753138e0 Code size: 0x00000150
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x00007fdf88a3c238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile'
# parm0: rsi:rsi = '[Ljava/lang/String;'
# [sp+0x40] (sp of caller)
#main方法入口
0x00007fdf753138e0: mov %eax,0xfffffffffffec000(%rsp)
0x00007fdf753138e7: push %rbp
0x00007fdf753138e8: sub $0x30,%rsp
0x00007fdf753138ec: movabs $0x7fdf88a3c300,%rdi ; {metadata(method data for {method} {0x00007fdf88a3c238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile')}
0x00007fdf753138f6: mov 0xdc(%rdi),%ebx
0x00007fdf753138fc: add $0x8,%ebx
0x00007fdf753138ff: mov %ebx,0xdc(%rdi)
0x00007fdf75313905: movabs $0x7fdf88a3c238,%rdi ; {metadata({method} {0x00007fdf88a3c238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile')}
0x00007fdf7531390f: and $0x0,%ebx
0x00007fdf75313912: cmp $0x0,%ebx
0x00007fdf75313915: je 0x7fdf75313941 ;*bipush
; - TestVolatile::main@0 (line 5)
#0xf066da08 + 0x68就是静态变量value的地址
0x00007fdf7531391b: movabs $0xf066da08,%rsi ; {oop(a 'java/lang/Class' = 'TestVolatile')}
#将0x9赋值给value
0x00007fdf75313925: movl $0x9,0x68(%rsi) ;*putstatic value
; - TestVolatile::main@5 (line 6)
#将value赋值给edi寄存器
0x00007fdf7531392c: mov 0x68(%rsi),%edi ;*getstatic value
; - TestVolatile::main@8 (line 7)
#将edi寄存器加10
0x00007fdf7531392f: add $0xa,%edi
#edi寄存器的值赋值给value
0x00007fdf75313932: mov %edi,0x68(%rsi) ;*putstatic value
; - TestVolatile::main@13 (line 7)
#将栈指针寄存器指向当前栈帧的栈底
0x00007fdf75313935: add $0x30,%rsp
#恢复帧指针寄存器
0x00007fdf75313939: pop %rbp
#返回
0x00007fdf7531393a: test %eax,0x176927c0(%rip) ; {poll_return}
0x00007fdf75313940: retq
截取main方法对应的汇编,发现这三个操作就是正常的汇编指令,1.首先将0x9赋值给value。2.然后将value赋值给edi寄存器。3.将edi寄存器加10。4.将edi寄存器的值赋值给value。
我们将value变为volatile变量,再看看对应的汇编。
public class TestVolatile{
public static volatile int value;
public static void main(String[] args) {
int a = 10;
value = 9;
value += a;
}
}
Argument 0 is unknown.RIP: 0x7f96b93132a0 Code size: 0x00000150
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x00007f96b7106238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile'
# parm0: rsi:rsi = '[Ljava/lang/String;'
# [sp+0x40] (sp of caller)
#main方法入口
0x00007f96b93132a0: mov %eax,0xfffffffffffec000(%rsp)
0x00007f96b93132a7: push %rbp
0x00007f96b93132a8: sub $0x30,%rsp
0x00007f96b93132ac: movabs $0x7f96b7106300,%rdi ; {metadata(method data for {method} {0x00007f96b7106238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile')}
0x00007f96b93132b6: mov 0xdc(%rdi),%ebx
0x00007f96b93132bc: add $0x8,%ebx
0x00007f96b93132bf: mov %ebx,0xdc(%rdi)
0x00007f96b93132c5: movabs $0x7f96b7106238,%rdi ; {metadata({method} {0x00007f96b7106238} 'main' '([Ljava/lang/String;)V' in 'TestVolatile')}
0x00007f96b93132cf: and $0x0,%ebx
0x00007f96b93132d2: cmp $0x0,%ebx
0x00007f96b93132d5: je 0x7f96b931330c ;*bipush
; - TestVolatile::main@0 (line 5)
0x00007f96b93132db: movabs $0xf066da08,%rsi ; {oop(a 'java/lang/Class' = 'TestVolatile')}
#将9赋值给edi寄存器
0x00007f96b93132e5: mov $0x9,%edi
#将edi寄存器的值赋值给value
0x00007f96b93132ea: mov %edi,0x68(%rsi)
#带lock前缀的加指令,把rsp所指向的地址中值加0,这个指令没啥用,主要使用lock前缀做内存屏障的
#防止lock之后的指令在lock之前执行,这里没使用mfence指令,主要是mfence在某些情况下比lock效率慢
0x00007f96b93132ed: lock addl $0x0,(%rsp) ;*putstatic value
; - TestVolatile::main@5 (line 6)
#将value的值赋值给edi寄存器
0x00007f96b93132f2: mov 0x68(%rsi),%edi ;*getstatic value
; - TestVolatile::main@8 (line 7)
#将edi寄存器加10
0x00007f96b93132f5: add $0xa,%edi
#将edi寄存器赋值给value
0x00007f96b93132f8: mov %edi,0x68(%rsi)
#加lock前缀做内存屏障,防止lock后的指令跑到lock前执行
0x00007f96b93132fb: lock addl $0x0,(%rsp) ;*putstatic value
; - TestVolatile::main@13 (line 7)
#从main方法返回
0x00007f96b9313300: add $0x30,%rsp
0x00007f96b9313304: pop %rbp
0x00007f96b9313305: test %eax,0x165dcdf5(%rip) ; {poll_return}
0x00007f96b931330b: retq
从汇编语言中可以看到在对volatile变量赋值后会加一条lock addl 0x0,(%rsp)指令,lock指令具有内存屏障的作用,lock前后的指令不会重排序,addl 0x0,(%rsp)是一条无意义的指令,在hotspot源码中内存屏障也是使用这样的指令实现的,没使用mfence指令,hotspot中解释说mfence有时候开销会很大。