CAS问题结合AtomicInteger分析

前言

公司业务渐渐要写多线程模块了,因为自己对多线程这块并不是非常了解,所以在bilibili上学习马士兵老师的高并发课程,以下也是听完第一节课后做的笔记。


CAS问题引入

在多线程环境下当我们使用AtmoicInteger中getAndIncrement()方法的当时候发现我们不需要加锁。他的源码其实时应用了CAS操作,这是一种无锁,也有人称自旋锁,也是乐观锁。

CAS

CAS图解

1.当一个线程进来拿到了当前值E(比如当前E是0,并将其存为N)。
2.开始计算,计算结果V(计算后当结果V为1)
3.计算完成后,比较N和E当值是否相等,如果相等就直接把E(0)值更新为V(1)。
如果不相等,就说明,E被其他线程改动过,如果改动之后为1,也就是当前E被改为1了,则重复上述动作,拿到当前值E(为1),计算结果为V(2),继续比较N(1)和E….

CAS存在的ABA问题

CAS存在ABA的问题,即一个线程A进来拿到当前值E(0),但是在修改过程中,同时两个线程进来对E进行了-1和+1的操作,由此当线程A要去比较原始值E的时候发现和当时读到的值是相同的,实际上这个E已经被两个线程修改过了,只不过修改过后的结果和最开始的E是一样的。由此引发了ABA的问题。

ABA问题的解决,通过加版本号,每次修改值E都需要改变其版本号,当比较原始值是否相同的时候同时也要比较版本号是否相同。版本号可以是多种形式(boolean、时间戳等)。


CAS问题AtomicInteger源码跟踪

在AtomicInteger中内部是由一个int域来保存值的,其由volatile关键字修饰,用于保证可见性。

1
private volatile int value;

其实不只是Atomic类中使用了compareAndSwap方法,像synchronized,volatile底层也是这么实现的,这个等之后在看,现在我们来看看AtmoicInteger的getAndIncrement()的源码

1
2
3
4
5
6
7
8
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

如果跟到JDK底层我们会发现他是Unsafe类中的一个方法

1
2
3
4
5
6
7
8
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

再往下面跟,我们发现一个native修饰的方法,这就意味着这已经不是java代码实现了,是C++或C实现的,再继续跟就要跟到HotSpot也就是JVM的源码中去了。

1
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

我们继续跟踪到Hotspot中到unsafe.cpp,中的实现源码如下

1
2
3
4
5
6
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

然后继续跟踪Atomic::cmpxchg(x, addr, e)atomic_linux_x86.inline.hpp,由于这边是linux系统所以由linux自己的实现,不同的操作系统实现不同。其中的汇编源码

1
2
3
4
5
6
7
8
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;
}

asm是汇编语言,直接和硬件打交道,我们发现cmpxchg语句硬件直接支持。

LOCK_IF_MP(%4)其中的MP指的是Machine Processprs如果一个CPU是需要用cmpxchg就可以了,但如果多个CPU还要加前面的LOCK指令。

最终的实现:是lock cmpxchg指令

硬件:lock指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)。

LOCK会锁定这块内存区域到缓存行(缓存行锁定)并写回主内存。

inter64位开发手册对lock指令对解释:
1.会将当前处理器缓存行对数据立即写回到系统内存。
2.这个写回内存到操作会引起在其他CPU缓存了该内存地址到数据无效(MESI协议)
3.提供内存屏障功能,使得lock前后指令不能重排序ß

具体实现分析原文链接

CAS是否真的是原子性

问题:如果在比较的时候,已经比较好了之后,但还没修改值之前,被其他线程修改了,那么其他线程的值会被当前值覆盖。

如果底层单单是一个cmpxchg指令,有多个CPU,他是不具有原子性的。但是在多个CPU的情况下,LOCK指令起到了关键性的作用,即一个CPU对一个值进行修改的时候,不允许其他CPU修改这个值。也是因为这个lock给cmpxchg提供了原子性。


如果有小伙伴,想要一起交流学习的,欢迎添加博主微信。

weChat