问题:cas怎么保证共享变量是原子性的?
1、demo演示
cas demo代码:
public class CasCounter {
private volatile int count = 0;
private static long offset;
private static Unsafe unsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
offset = unsafe.objectFieldOffset(CasCounter.class.getDeclaredField("count"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void increment() {
int before = count;
// 失败了就重试直到成功为止
while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) {
before = count;
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
CasCounter casCounter = new CasCounter();
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
threadPool.submit(() -> {
for (int j =0; j < 10000; j++) {
casCounter.increment();
}
});
}
threadPool.shutdown();
Thread.sleep(2000);
// 打印1000000
System.out.println(casCounter.getCount());
}
}
其中compareAndSwapInt是native方法,此处可以看出,原子性是在native方法的具体实现中保证的,那该native方法是如何保证共享变量的原子性的?接下来进行compareAndSwapInt源码的阅读。
2、compareAndSwapInt底层源码阅读
2.1、源码获取
可在openjdk中找到相关native源码:
https://github.com/AdoptOpenJDK/openjdk-jdk8u
这里以jdk8u为例,具体位置如下:
链接:http://hg.openjdk.java.net/jdk8u/jdk8u60/hotspot/file/37240c1019fd/src/share/vm/prims/unsafe.cpp
2.2、compareAndSwapInt源码
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量获取内存地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 执行内联汇编程序
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
其中windows对应的cmpxchg()方法如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
linux对应的cmpxchg()方法如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
可以看到cmpxchg是使用了汇编语言,最核心的比较并交换功能是通过指令cmpxchg实现的,指令具体含义可以参考如下链接:
https://www.felixcloutier.com/x86/cmpxchg
这里windows中只对cmpxchg指令加了LOCK前缀,如果eax、ecx、edx寄存器内的数据仍然是共享的,还是存在数据修改的可能,我的理解是这样的:cpu上下文切换时,会保存cpu上下文数据(包括寄存器和程序计数器),然后加载下一个线程,直到再次调度上一个线程时,把之前保存的上下文数据再次加载进来,保证上一个线程不受影响。也就是说,__asm代码块前和后有保存和恢复机制。
参考来源:http://www.linfo.org/context_switch.html
Context switching can be described in slightly more detail as the kernel (i.e., the core of the operating system) performing the following activities with regard to processes (including threads) on the CPU: (1) suspending the progression of one process and storing the CPU's state (i.e., the context) for that process somewhere in memory, (2) retrieving the context of the next process from memory and restoring it in the CPU's registers and (3) returning to the location indicated by the program counter (i.e., returning to the line of code at which the process was interrupted) in order to resume the process.
When using
__asm
to write assembly language in C/C++ functions, you don't need to preserve the EAX, EBX, ECX, EDX, ESI, or EDI registers. For example, in the POWER2.C example in Writing Functions with Inline Assembly, thepower2
function doesn't preserve the value in the EAX register. However, using these registers will affect code quality because the register allocator cannot use them to store values across__asm
blocks. In addition, by using EBX, ESI or EDI in inline assembly code, you force the compiler to save and restore those registers in the function prologue and epilogue.
参考:
https://www.jb51.net/article/161858.htm