一、什么是CAS鎖
概述
CAS的全稱為Compare-And-Swap,直譯就是對比交換。是一條CPU的原子指令,其作用是讓CPU先進行比較兩個值是否相等,然后原子地更新某個位置的值。經(jīng)過調(diào)查發(fā)現(xiàn),其實現(xiàn)方式是基于硬件平臺的匯編指令,就是說CAS是靠硬件實現(xiàn)的,JVM只是封裝了匯編調(diào)用,那些AtomicInteger類便是使用了這些封裝后的接口。 簡單解釋:CAS操作需要輸入兩個數(shù)值,一個舊值(期望操作前的值)和一個新值,在操作期間先比較下在舊值有沒有發(fā)生變化,如果沒有發(fā)生變化,才交換成新值,發(fā)生了變化則不交換。
原理
CAS有3個操作數(shù),位置內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的更新值B。
當(dāng)且僅當(dāng)舊的預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做或重來 。
硬件級別保證
CAS是JDK提供的非阻塞原子性操作,它通過 硬件保證了比較-更新的原子性。
它是非阻塞的且自身原子性,也就是說這玩意效率更高且通過硬件保證,說明這玩意更可靠
示例代碼
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
//期望時5,如果是5則改成2022
System.out.println(atomicInteger.compareAndSet(5, 2022) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2022) + "\t" + atomicInteger.get());
}
}
輸出結(jié)果
由于前面修改了,后面修改失敗,故先true后false。
源碼分析compareAndSet(int expect,int update)
compareAndSet()方法的源代碼:
上面三個方法都是類似的,主要對4個參數(shù)做一下說明。
var1:表示要操作的對象
var2:表示要操作對象中屬性地址的偏移量
var4:表示需要修改數(shù)據(jù)的期望的值
var5/var6:表示需要修改為的新值
引出來一個問題:UnSafe類是什么?
二、CAS底層原理
Unsafe
unsafe是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地(native)方法來訪問,Unsafe相當(dāng)于一個后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。 Unsafe類存在于sun.misc包中 ,其內(nèi)部方法操作可以像C的指針 一樣直接操作內(nèi)存,因為Java中CAS操作的執(zhí)行依賴于Unsafe類的方法。
注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù)
valueOffset
變量valueOffset,表示該變量值在內(nèi)存中的 偏移地址 ,因為Unsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的
volatile
變量value用volatile修飾,保證了多線程之間的內(nèi)存可見性。
源碼分析
OpenJDK源碼里面查看下 Unsafe.java
假設(shè)線程A和線程B兩個線程同時執(zhí)行g(shù)etAndAddInt操作(分別跑在不同CPU上):
1 AtomicInteger里面的value原始值為3,即主內(nèi)存中AtomicInteger的value為3,根據(jù)JMM模型,線程A和線程B各自持有一份值為3的value的副本分別到各自的工作內(nèi)存。
2 線程A通過getIntVolatile(var1, var2)拿到value值3,這時線程A被掛起。
3 線程B也通過getIntVolatile(var1, var2)方法獲取到value值3,此時剛好線程B 沒有被掛起 并執(zhí)行compareAndSwapInt方法比較內(nèi)存值也為3,成功修改內(nèi)存值為4,線程B打完收工,一切OK。
4 這時線程A恢復(fù),執(zhí)行compareAndSwapInt方法比較,發(fā)現(xiàn)自己手里的值數(shù)字3和主內(nèi)存的值數(shù)字4不一致,說明該值已經(jīng)被其它線程搶先一步修改過了,那A線程本次修改失敗, 只能重新讀取重新來一遍了。
5 線程A重新獲取value值,因為變量value被volatile修飾,所以其它線程對它的修改,線程A總是能夠看到,線程A繼續(xù)執(zhí)行compareAndSwapInt進行比較替換,直到成功。
底層匯編
native修飾的方法代表是底層方法
Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現(xiàn)位于unsafe.cpp中
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);
// 先想辦法拿到變量value在內(nèi)存中的地址,根據(jù)偏移量valueOffset,計算 value 的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 調(diào)用 Atomic 中的函數(shù) cmpxchg來進行比較交換,其中參數(shù)x是即將更新的值,參數(shù)e是原內(nèi)存的值
return (jint) (Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
(Atomic::cmpxchg(x, addr, e)) == e; (主要源碼)
cmpxchg
調(diào)用 Atomic 中的函數(shù) cmpxchg來進行比較交換,其中參數(shù)x是即將更新的值,參數(shù)e是原內(nèi)存的值
return (jint) (Atomic::cmpxchg(x, addr, e)) == e;
unsigned Atomic:: cmpxchg (unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
/*
* 根據(jù)操作系統(tǒng)類型調(diào)用不同平臺下的重載函數(shù),這個在預(yù)編譯期間編譯器會決定調(diào)用哪個平臺下的重載函數(shù)*/
return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}
總結(jié)
你只需要記?。?/p>
- CAS是靠硬件實現(xiàn)的從而在硬件層面提升效率,最底層還是交給硬件來保證原子性和可見性
- 實現(xiàn)方式是基于硬件平臺的匯編指令,在intel的CPU中(X86機器上),使用的是匯編指令cmpxchg指令。
核心思想就是:比較要更新變量的值V和預(yù)期值E(compare),相等才會將V的值設(shè)為新值N(swap)如果不相等自旋再來。
三、原子引用
在上面我們知道AtomicInteger原子整型,那可否有其它原子類型呢?
比如說:AtomicBook、AtomicOrder
答案是肯定的。這里引入AtomicReference
AtomicReference示例
@Data
@AllArgsConstructor
class User{
String username;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 24);
User li4 = new User("li4", 26);
AtomicReference<User> atomicReferenceUser = new AtomicReference<>();
atomicReferenceUser.set(z3);
System.out.println(atomicReferenceUser.compareAndSet(z3, li4) + "\t" + atomicReferenceUser.get().toString());
System.out.println(atomicReferenceUser.compareAndSet(z3, li4) + "\t" + atomicReferenceUser.get().toString());
}
}
四、自旋鎖,借鑒CAS思想
什么是自旋鎖?
自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環(huán)的方式 去嘗試獲取鎖 ,
當(dāng)線程發(fā)現(xiàn)鎖被占用時,會不斷循環(huán)判斷鎖的狀態(tài),直到獲取。這樣的好處是減少線程上下文切換的消耗,缺點是循環(huán)會消耗CPU 。
示例
題目:實現(xiàn)一個自旋鎖
自旋鎖好處:循環(huán)比較獲取沒有類似 wait 的阻塞。
通過 CAS 操作完成自旋鎖。
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t" + "---come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t" + "---task over , unlock ...");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
spinLockDemo.unlock();
}, "A").start();
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unlock();
}, "B").start();
}
}
輸出結(jié)果
A線程先進,B線程后進。緊接著A線程等待,然后解鎖,B線程在A線程解鎖后才會解鎖。
解析
A 線程先進來調(diào)用 myLock 方法自己持有鎖 5 秒鐘, B 隨后進來后發(fā)現(xiàn)
當(dāng)前有線程持有鎖,不是 null ,所以只能通過自旋等待,直到 A 釋放鎖后 B 隨后搶到。
這種自旋等待嘗試的過程就是自旋鎖。
五、CAS的缺點
循環(huán)時間長開銷很大
我們可以看到getAndAddInt方法執(zhí)行時,有個do while 。
如果CAS失敗,會一直進行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。
引出來ABA問題
ABA問題怎么產(chǎn)生的
CAS算法實現(xiàn)一個重要前提需要取出內(nèi)存中某時刻的數(shù)據(jù)并在當(dāng)下時刻比較并替換,那么在這個時間差類會導(dǎo)致數(shù)據(jù)的變化。
比如說一個線程one從內(nèi)存位置V中取出A,這時候另一個線程two也從內(nèi)存中取出A,并且線程two進行了一些操作將值變成了B,
然后線程two又將V位置的數(shù)據(jù)變成A,這時候線程one進行CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,然后線程one操作成功。文章來源:http://www.zghlxwxcb.cn/news/detail-594923.html
盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。文章來源地址http://www.zghlxwxcb.cn/news/detail-594923.html
到了這里,關(guān)于「JUC并發(fā)編程」初識CAS鎖(概述、底層原理、原子引用、自旋鎖、缺點)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!