共计 4869 个字符,预计需要花费 13 分钟才能阅读完成。
学习 Java 并发编程,CAS 机制都是一个不得不把握的知识点。这篇文章次要是从呈现的起因再到原理进行一个解析。心愿对你有所帮忙。
一、为什么须要 CAS 机制?
为什么须要 CAS 机制呢?咱们先从一个谬误景象谈起。咱们常常应用 volatile 关键字润饰某一个变量,表明这个变量是全局共享的一个变量,同时具备了可见性和有序性。然而却没有原子性。比如说一个常见的操作 a ++。这个操作其实能够细分成三个步骤:
(1)从内存中读取 a
(2)对 a 进行加 1 操作
(3)将 a 的值从新写入内存中
在单线程状态下这个操作没有一点问题,然而在多线程中就会呈现各种各样的问题了。因为可能一个线程对 a 进行了加 1 操作,还没来得及写入内存,其余的线程就读取了旧值。造成了线程的不平安景象
。如何去解决这个问题呢?最常见的形式就是应用AtomicInteger
来润饰a
。咱们能够看一下代码:
// 应用 AtomicInteger 定义 a
static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) {Test3 test = new Test3();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {threads[i] = new Thread(() -> {
try {for (int j = 0; j < 10; j++) {
// 应用 getAndIncrement 函数进行自增操作
System.out.println(a.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {e.printStackTrace();
}
});
threads[i].start();}
}
}
当初咱们应用 AtomicInteger 类并且调用了 incrementAndGet 办法来对 a 进行自增操作。这个 incrementAndGet 是如何实现的呢?咱们能够看一下 AtomicInteger 的源码。
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
咱们到这一步能够看到其实就是 usafe 调用了 getAndAddInt 的办法实现的,然而当初咱们还看不出什么,咱们再深刻到源码中看看 getAndAddInt 办法又是如何实现的,
int var5;
do {var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
到了这一步就略微有点头绪了,原来底层调用的是 compareAndSwapInt
办法,这个 compareAndSwapInt
办法其实就是 CAS 机制。因而如果咱们想搞清楚 AtomicInteger
的原子操作是如何实现的,咱们就必须要把 CAS 机制搞清楚,这也是为什么咱们须要把握 CAS 机制的起因。
二、剖析 CAS
1、根本含意
CAS 全拼又叫做compareAndSwap
,从名字上的意思就晓得是比拟替换的意思。比拟替换什么呢?
过程是这样:它蕴含 3 个参数 CAS(V,E,N),V 示意要更新变量的值,E 示意预期值,N 示意新值。仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则阐明曾经有其余线程做两个更新,则以后线程则什么都不做。最初,CAS 返回以后 V 的实在值。
咱们举一个我之前举过的例子来阐明这个过程:
比如说给你儿子订婚。你儿子就是内存地位,你本来认为你儿子是和杨贵妃在一起了,后果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你料想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。当初你应该明确了吧。
CAS 操作时抱着乐观的态度进行的,它总是认为本人能够胜利实现操作。所以 CAS 也叫作乐观锁,那什么是乐观锁呢?乐观锁就是咱们之前赫赫有名的synchronized
。乐观锁的思维你能够这样了解,一个线程想要去取得这个锁然而却获取不到,必须要他人开释了才能够。
2、底层原理
想要弄清楚其底层原理,深刻到源码是最好的形式,在下面咱们曾经通过源码看到了其实就是 Usafe
的办法来实现的,在这个办法中应用了 compareAndSwapInt
这个 CAS 机制。因而,当初咱们有必要进一步深刻进去看看:
// compareAndSwapInt 是 native 类型的办法
public final native boolean compareAndSwapInt(
Object o,
long offset,
int expected,
int x
);
// 残余还有很多办法
}
咱们能够看到这外面次要有四个参数,第一个参数就是咱们操作的对象 a,第二个参数是对象 a 的地址偏移量,第三个参数示意咱们期待这个 a 是什么值,第四个参数示意的是 a 的理论值。
不过这里咱们会发现这个 compareAndSwapInt
是一个 native
办法,也就是说再往下走就是 C 语言代码,如果咱们放弃好奇心,能够持续深刻进去看看。
jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 依据偏移量 valueOffset,计算 value 的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg 来进行比拟替换
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
下面的代码咱们解读一下:首先应用 jint
计算了 value
的地址,而后依据这个地址,应用了 Atomic
的cmpxchg
办法进行比拟替换。当初问题又抛给了这个cmpxchg
,实在实现的是这个函数。咱们再进一步深刻看看,假相曾经离咱们不远了。
volatile unsigned int* dest,
unsigned int compare_value) {assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
/*
* 依据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
*/
return (unsigned int)Atomic::cmpxchg((jint)exchange_value,
(volatile jint*)dest, (jint)compare_value);
}
皮球又一次被完满的踢走了,当初在不同的操作系统下会调用不同的 cmpxchg
重载函数,我当初用的是 win10 零碎,所以咱们看看这个平台下的实现,别着急再往下走走:
jint compare_value) {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
}
}
这块的代码就有点波及到汇编指令相干的代码了,到这一步就彻底靠近假相了,首先 三个 move 指令
示意的是将前面的值挪动到后面的寄存器上。而后调用了 LOCK_IF_MP
和上面 cmpxchg
汇编指令进行了比拟替换。当初咱们不晓得这个 LOCK_IF_MP
和cmpxchg
是如何替换的,没关系咱们最初再深刻一下。
假相来了,他来了,他真的来了。
volatile jint* dest, jint compare_value) {
//1、判断是否是多核 CPU
int mp = os::is_MP();
__asm {
//2、将参数值放入寄存器中
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
//3、LOCK_IF_MP 指令
cmp mp, 0
//4、如果 mp = 0,表明线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处,间接执行 cmpxchg 指令
je L0
_emit 0xF0
//5、这里真正实现了比拟替换
L0:
/*
* 比拟并替换。简略解释一下上面这条指令,相熟汇编的敌人能够略过上面的解释:
* cmpxchg: 即“比拟并替换”指令
* dword: 全称是 double word 示意两个字,一共四个字节
* ptr: 全称是 pointer,与后面的 dword 连起来应用,表明拜访的内存单元是一个双字单元
* 这一条指令的意思就是:将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行比照,如果雷同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。*/
cmpxchg dword ptr [edx], ecx
}
}
到这一步了,置信你应该了解了这个 CAS 真正实现的机制了吧,最终是由操作系统的汇编指令实现的。
3、CAS 机制的优缺点
(1)长处
一开始在文中咱们已经提到过,cas 是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁
,什么是非阻塞式的呢?其实就是一个线程想要取得锁,对方会给一个回应示意这个锁能不能取得。在资源竞争不强烈的状况下性能高,相比synchronized
分量锁,synchronized
会进行比较复杂的加锁,解锁和唤醒操作。
(2)毛病
毛病也是一个十分重要的知识点,因为波及到了一个十分驰名的问题,叫做 ABA
问题。假如一个变量 A,批改为 B 之后又批改为 A,CAS 的机制是无奈觉察的,但实际上曾经被批改过了。这就是 ABA
问题,
ABA 问题会带来大量的问题,比如说数据不统一的问题等等。咱们能够举一个例子来解释阐明。
你有一瓶水放在桌子上,他人把这瓶水喝完了,而后从新倒上去。你再去喝的时候发现水还是跟之前一样,就误以为是刚刚那杯水。如果你晓得了假相,那是他人用过了你还会再用嘛?举一个比拟黄一点的例子,
女朋友被他人睡过之后又回来,还是之前的那个女朋友嘛
?
ABA 能够有很多种形式来解决,在其余的文章曾经给出。