Jave 面試 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就拿到鎖了,然後跳出循環,最終解鎖~

唉…剛寫完了!別白嫖啊,點贊關注,給你們福利啊~~轉載請標註!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章