CAS就這?
好兄弟們,不會真有人看不懂CAS吧?反正我是沒看懂…
一. CAS是什麼?
import java.util.concurrent.atomic.AtomicInteger;
/**
* 1. CAS是什麼? => compareAndSet 比較並交換
*/
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019)+"\t "+"currentData: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,2020)+"\t "+"currentData: "+atomicInteger.get());
}
}
周陽老師的圖就是畫的騷~
怎麼個意思呢
- 一開始,我給主物理內存設置值爲5
- 第一個線程來了,要跟內存比較並交換,線程的期望值是 5 ,而剛好內存值就是5, 然後就交換了值,也就是把主物理內存的值5改爲了2019,然後返回了個true代表取到的值與期望值是一樣的
- 然後通知其他線程可見了,第二個線程來了,發現主物理內存是2019,跟自己的期望值5不一樣啊,然後就返回了個false,主物理內存並沒有改變~.
二. CAS底層原理
首先來看看atomicInteger.getAndIncrement()爲什麼不加synchronized也能在多線程下保持線程安全
點開後,我們發現有個unsafe類,unsafe是CAS的核心類
- 1. Unsafe
是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(native) 方法來訪問,Unsafe相當於-一個後門,基於該類可以直接操作特定內存的數據。Unsafe類 存在於sun.misc包中,其內部方法操作可以像C的指針一-樣直接操作內存,因爲Java中Unsafe是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(native) 方法來訪問,Unsafe相當於一個後門,基於該類可以直接操作特定內存的數據。Unsafe類存在於sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因爲Java中CAS操作的執行依賴於Unsafe類的方法。注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應任務
注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應任務
-
2. 變量valueOffset, 表示該變量值在內存中的偏移地址,因爲Unsafe就是根據內存偏移地址獲取數據的。
-
3. 變量value用volatile修飾, 保證了多線程之間的內存可見性。
2.1 JMM內存模型(涉及到的知識點)
由於JMM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到的線程自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:
2.2 CAS底層
CAS的全稱爲Compare-And-Swap,它是一-條CPU併發原語。
它的功能是判斷內存某個位置的值是否爲預期值,如果是則更改爲新的值,這個過程是原子的。
CAS併發原語體現在JAVA語言中就是sun.misc.Unsafe類中的各個方法。調用UnSafe類中的CAS方法,JVM會幫我們實現出CAS彙編指令。這是一種完全依賴於硬件的功能,通過它實現了原子操作。再次強調,由於CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會 造成所謂的數據不一致問題。
首先, var1代表當前對象,var2代表對象的偏移地址,var4就是那個+1的值,然後getIntVolatile()這個方法去獲取當前對象的這個值是多少,給他保存到var5,然後,過了一會,compareAndSwapInt()這個方法去再比較當前對象的值還是不是var5,是的話就給這個值+1,返回true,while裏面就是false就退出循環,最後返回出+1後的值. 如果當前對象不是之前的var5了,返回一個false,while循環裏面就是true,繼續循環,拿到下一個值去比較,直到比較成功~
再來一遍,根據JMM內存模型來看
- 1. 假設有兩個線程AB,根據上面的內存模型來看 AtomicInteger 裏面的value原始值爲5,即主內存中AtomicInteger的value爲5,根據JMM模型,線程A和線程B各自持有一份值爲5的value的副本分別到各自的工作內存。
- 2. 線程A通過getIntVolatile(var1, var2)拿到value值5, 這時線程A被掛起。
- 3. 線程B也通過getlntVolatile(var1, var2)方法獲取到value值5, 此時剛好線程B沒有被掛起並執行compareAndSwapInt方法比較內存值也爲5,成功修改內存值爲6,線程B打完收工,一切OK。
- 4. 這時線程A恢復,執行compareAndSwapInt方法比較, 發現自己手裏的值數字5和主內存的值數字6不一致,說明該值已經被其它線程搶先一步修改過了,那A線程本次修改失敗,只能重新讀取新來一遍了。
- 5. 線程A重新獲取value值, 因爲變量value被volatile修飾, 所以其它線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功。
2.3 總結與應用
1. CAS (CompareAndSwap)總結
比較當前工作內存中的值和主內存中的值,如果相同則執行規定操作,
否則繼續比較直到主內存和工作內存中的值一致爲止.
2. CAS應用
CAS有3個操作數,內存值V;舊的預期值A,要修改的更新值B。
當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。.
3. CAS缺點
- 由於用了while循環,當在線程數比較大的情況下,如果失敗可能會出現一直循環,導致CPU過高
- 只能保證一個共享變量的原子操作。
- 引出來ABA問題(反正就是狸貓換太子把戲)
三. 自旋鎖SpinLock
是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU
寫了這麼久代碼,這不就是個自旋鎖嘛!
代碼貼出來
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋鎖Demo
*/
public class SpinLockDemo {
AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void myLock(){
Thread thread=Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t "+"come in ^0^");
while(!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread=Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t "+ "invoke myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo=new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
},"AAA").start();
try {
TimeUnit.SECONDS.sleep(1); //保證線程絕對運行完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1); //加鎖後延遲1s
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
},"BBB").start();
}
}
怎麼碩呢,大家看懂了以上代碼就理解了什麼是自選鎖了
- 這裏我開始拿了個原子線程
- 定義一個加鎖方法,是自旋鎖,如果當前線程是空就把當前線程更新進去CAS,並且跳出循環,如果當前線程不是空,就會一直在while循環裏一直判斷
- 定義一個解鎖辦法,獲取當前線程,如果原子線程還是當前線程,那就把它設置爲null
- 主方法中,我先讓線程AAA獲取鎖,並且sleep5秒鐘,這個時候當前線程就會一直是AAA線程
- 然後BBB線程進來了,發現當前線程並不是null,而是AAA,就回一直循環判斷當前線程什麼時候爲null,等到5秒後,線程AAA解鎖了把當前原子線程釋放掉了,這時候BB就拿到鎖了,然後跳出循環,最終解鎖~