C 语言中的volatile关键字作用?
易变性:volatile告诉编译器,某个变量是易变的,当编译器遇到这个变量的时候,只能从变量的内存地址中读取这个变量,不可以从缓存、寄存器、或者其它任何地方读取。
顺序性:两个包含volatile变量的指令,编译后不可以乱序。注意是编译后不乱序,但是在执行的过程中还是可能会乱序的,这点需要由其它机制来保证,例如memory- barriers。
Java中的volatile关键字作用?
Java的volatile关键字在语义上有所增强,主要体现在:
Java Volatile变量的操作,附带了Acquire与Release语义。所谓的Acquire与Release语义,可参考文章:Acquire and Release Semantics。(这一点,后续有必要的话,可以写一篇文章专门讨论)。Java Volatile所支持的Acquire、Release语义,如下:
- 对于Java Volatile变量的写操作,带有Release语义,所有Volatile变量写操作之前的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的写操作之后执行。
- 对于Java Volatile变量的读操作,带有Acquire语义,所有Volatile变量读操作之后的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到volatile变量的读操作之前进行。
通过Java Volatile的Acquire、Release语义,对比C/C++ Volatile,可以看出,Java Volatile对于编译器、CPU的乱序优化,限制的更加严格了。Java Volatile变量与非Volatile变量的一些乱序操作,也同样被禁止。
什么是happens-before?
令A和B表示两组操作,如果A happens-before B,那么由A操作引起的内存变化,在B开始执行之前,都应该是可见的。
A happens-before B,不代表A在B之前执行!
int A = 0;
int B = 0;
void foo()
{
A = B + 1; // (1)
B = 1; // (2)
}
上述的代码,满足A happens-before B,但是语句(2)可能在(1)之前执行。
所以如果有另外一个线程,尝试去读取B的值,然后再读A的值,那么可能就会有问题:
if (B == 1)
{
assert(A == 2); // 错误!A可能还是0
}
如何确保happen-before?
锁(互斥锁、读写锁等)、内存屏障
指令乱序
指令乱序是指程序在执行代码的时候,并不按代码编写的实际顺序执行。这种情况可能发生在两个阶段,一个是编译阶段,一个是运行阶段。
关于编译阶段的乱序,参考资料中何登成的博客中给出了示例,这里就不赘述。C的volatile关键字,只能保证编译阶段不乱序,但是不能保证运行时不乱序。
什么是内存屏障(memory barriers)?
内存屏障是一个指令,这个指令可以保证屏障前后的指令遵守一定的顺序,并且保证一定的可见性,具体的,在典型的Inter X86架构上,有以下几种内存屏障指令:
- sfence:保证此fence指令之前的所有写操作,先于(happens-before)屏障之后的所有写操作。
- lfence:保证此fence指令之前的所有读操作,先于(happens-before)屏障之后的所有读操作。
- mfence:相当于sfence + lfence。
关于常见的内存屏障,见:MemoryBarriers
另附自己使用内存屏障实现的一个无锁类似ring buffer的实现:https://github.com/jiafujiang/SampleC/tree/master/src/ring_buffer_sample
ping-pong时间大概在几百ns这个级别,不同的机器会有不同的表现。
另外附上朋友写的一个Java版的实现:https://github.com/nottryhard/something/blob/master/src/RingBuffer.java