java 無鎖實現原理CAS解析

以一段取款餘額引出問題

  • 賬戶餘額提取問題
public interface Account {

    public static void main(String[] args) {
        // 不安全 無鎖
        Account accountUnsafe = new AccountUnsafe(10000);
        Account.demo(accountUnsafe);

    }
    

    // 獲取餘額 
    Integer getBalance();
    

    // 取款
    void withdraw(Integer amount);
    
    /**
     * 方法內會啓動 1000 個線程,每個線程做 -10 元 的操作
     * 如果初始餘額爲 10000 那麼正確的結果應當是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        //創建1000個線程
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end-start)/1000_000 + " ms");
    }
}




  • 實現一不安全
class AccountUnsafe implements Account{

        //餘額
        private Integer balance;

        public AccountUnsafe(Integer balance) {
            this.balance = balance;
        }

        @Override
        public Integer getBalance() {
            return this.balance;
        }

        @Override
        public void withdraw(Integer amount) {
          this.balance -= amount;
        }
    }
    
    //執行
    public static void main(String[] args) {
        // 不安全 無鎖
       Account accountUnsafe = new AccountUnsafe(10000);
       Account.demo(accountUnsafe);

    }
    
    結果: 250 cost: 197 ms

結論:1000個線程對賬號爲10000餘額扣減10,結果應爲0。因爲出現了共享資源的競爭問題,多線程導致結果出錯。

解決方式-加鎖

 class AccountSynchronized implements Account{

        //餘額
        private Integer balance;

        public AccountSynchronized(Integer balance) {
            this.balance = balance;
        }

        @Override
        public Integer getBalance() {
            return this.balance;
        }

        @Override
        public void withdraw(Integer amount) {
           synchronized (this){
               this.balance -= amount;
           }
        }
    }
    //執行
     public static void main(String[] args) {
   
       // synchronize 加鎖
        Account accountSynchronize = new AccountSynchronized(10000);
        Account.demo(accountSynchronize);

    }
    結果: 0 cost: 209 ms

結論:結果正確

解決方式-無鎖

 class AccountCas implements Account{

        //餘額
        private AtomicInteger balance;

        public AccountCas(Integer balance) {
            this.balance = new AtomicInteger(balance);
        }

        @Override
        public Integer getBalance() {
            return balance.get();
        }

        @Override
        public void withdraw(Integer amount) {
            //CPU 指令級別 原子操作不可分割
           while(true){
               int prve = balance.get();
               int next = prve - amount;
               // CAS 比較並設置 機制:會以prve與當前最新的balance值作比較,如果過相同則將值設置爲next
               // 若失敗則不斷進行嘗試
               if(balance.compareAndSet(prve,next)){
                   break;
               }
           }
        }
    }
    //執行
     public static void main(String[] args) {
   
       // CAS 無鎖
        Account accountCas = new AccountCas(10000);
        Account.demo(accountCas);

    }
    結果: 0 cost: 279 ms

結論:結果正確


Cas與volatile

  • CAS
 public void withdraw(Integer amount) {
            // 需要不斷嘗試,直到成功爲止
           while(true){
               //獲取舊值100
               int prve = balance.get();
               // next = 100 - 10 = 90
               int next = prve - amount;
               // CAS 比較並設置 機制:會以prve與當前最新的balance值作比較,如果過相同則將值設置爲next
               // compareAndSet 正是做這個檢查,在 set 前,先比較 prev 與當前值
               //不一致了,next 作廢,返回 false 表示失敗
               //比如,別的線程已經做了減法,當前值已經被減成了 90
               //那麼本線程的這次 90 就作廢了,進入 while 下次循環重試
               //一致,以 next 設置爲新值,返回 true 表示成功
               if(balance.compareAndSet(prve,next)){
                   break;
               }
           }
        }

其中的關鍵是 compareAndSet,它的簡稱就是 CAS (也有 Compare And Swap 的說法),它必須是原子操作。

線程一Account賬戶線程二獲取餘額100減10 = 90修改爲90cas(100,90)失敗獲取餘額90修改爲80減10 = 80cas(90,80)失敗減10 = 70cas(80,70)成功線程一Account賬戶線程二

注意 cas底層是在cpu指令上lock cmpxchg,在單核cpu與多核cpu都能保證【比較-交換】的原子性。
在多核的cpu下,某一個核執行帶lock的指令,CPU會讓總線鎖住,當這個核把指令執行完畢,在開啓總線。這個過程中指令的執行不會被線程調度機制鎖打斷,保證多線程對內存操作的原子性。

  • volatile

獲取共享變量時,爲了保持可見性需要使用volatile

volatile可以修飾成員變量與靜態成員變量,防止變量從工作緩存中獲取變量,必須從主存中獲取變量,線程操作volatile變量直接操作主存,即線程對volatile修改對另一個線程可見

注意: volatile只能解決線程的可見性問題,不能解決指令交錯問題(不能保證原子性)

Cas必須使用volatile變量 保證共享變量的可見性,才能實現【比較與交換】

以AtomicInteger爲例:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 內部維護了value 被volatile所修飾
    private volatile int value;

無鎖效率相對高

  • 在無鎖的狀態下,即使重試失敗,線程始終處於高速的運行狀態下,而使用sychronied會讓線程在沒有鎖的情況下,上下文切換,進入阻塞。
  • 舉個例子:線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車要減速、熄火,
    等被喚醒又得重新打火、啓動、加速… 恢復到高速運行,代價比較大
  • 在無鎖的情況,保持線程的運行,需要額外的CPU支持。CPU相當於跑道,線程的運行無從談起,雖然不會進入阻塞,但是由於沒分到時間片,線程處於可運行狀態,但是依然會導致線程的上下文切換

CAS的特點

結合CAS與volatile的特點,無鎖使用與線程數少,CPU核數多的情況

  • CAS無鎖爲基於樂觀鎖思想:樂觀的估計,不怕其他線程修改共享變量,就算改了,在進行重試
  • Synchronized基於悲觀鎖思想:悲觀的估計,防止其他線程修改共享變量,修改完成後在解鎖。
  • CAS無鎖併發,無阻塞併發:
    • 因爲不需要進行線程的上下文切換,所以效率很高
    • 但是在競爭激烈的情況,頻繁的重試,反而影響效率
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章