高並發之CAS機制和ABA問題


什么是CAS機制

CAS是英文單詞Compare and Swap的縮寫,翻譯過來就是比較並替換

CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。

看如下幾個例子:

package com.example.demo.concurrentDemo;

import org.junit.Test;

import java.util.concurrent.atomic.AtomicInteger;

public class CasTest {

    private static int count = 0;

    @Test
    public void test1(){
        for (int j = 0; j < 2; j++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //結果必定  count <= 20000
        System.out.println(count);
    }

    @Test
    public void test2() {
        for (int j = 0; j < 2; j++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    synchronized (this) {
                        count++;
                    }
                }
            }).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //synchronized  類似於悲觀鎖
        //synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而后在爭奪到鎖資源后恢復為RUNNABLE狀態
        //這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高
        System.out.println(count);
    }

    private static AtomicInteger atoCount = new AtomicInteger(0);

    @Test
    public void test3() {
        for (int j = 0; j < 2; j++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    atoCount.incrementAndGet();
                }
            }).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //Atomic操作類的底層正是用到了“CAS機制”
        System.out.println(atoCount);
    }

}

CAS 缺點

1) CPU開銷過大

在並發量比較高的情況下,如果許多線程反復嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。

這個可以通過看:AtomicInteger.incrementAndGet()源碼,可知這是一個無限循環,獲取實際值與預期值比較,當相等才會跳出循壞。

2) 不能保證代碼塊的原子性

CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。

3) ABA問題

這是CAS機制最大的問題所在。

 

什么是ABA?先看下面例子:

我們先來看一個多線程的運行場景:
時間點1 :線程1查詢值是否為A 
時間點2 :線程2查詢值是否為A 
時間點3 :線程2比較並更新值為B 
時間點4 :線程2查詢值是否為B 
時間點5 :線程2比較並更新值為A 
時間點6 :線程1比較並更新值為C

在這個線程執行場景中,2個線程交替執行。線程1在時間點6的時候依然能夠正常的進行CAS操作,盡管在時間點2到時間點6期間已經發生一些意想不到的變化, 但是線程1對這些變化卻一無所知,因為對線程1來說A的確還在。通常將這類現象稱為ABA問題。
ABA發生了,但線程不知道。又或者鏈表的頭在變化了兩次后恢復了原值,但是不代表鏈表就沒有變化。

ABA隱患

就像兵法講的:偷梁換柱、李代桃僵 

歷史事件:趙氏孤兒

 解決ABA問題兩種方法:

1、悲觀鎖思路,加鎖;

2、樂觀鎖思路,通過AtomicStampedReference.class 

源碼實現,具體看源碼:

1. 創建一個Pair類來記錄對象引用和時間戳信息,采用int作為時間戳,實際使用的時候時間戳信息要做成自增的,否則時間戳如果重復,還會出現ABA的問題。這個Pair對象是不可變對象,所有的屬性都是final的, of方法每次返回一個新的不可變對象。

2. 使用一個volatile類型的引用指向當前的Pair對象,一旦volatile引用發生變化,變化對所有線程可見。

3. set方法時,當要設置的對象和當前Pair對象不一樣時,新建一個不可變的Pair對象。

4. compareAndSet方法中,只有期望對象的引用和版本號和目標對象的引用和版本好都一樣時,才會新建一個Pair對象,然后用新建的Pair對象和原理的Pair對象做CAS操作。

5. 實際的CAS操作比較的是當前的pair對象和新建的pair對象,pair對象封裝了引用和時間戳信息。

Demo:

 

 @Test
    public void test4() {
        final int timeStamp = atoReferenceCount.getStamp();

        new Thread(() -> {
            while(true){
                if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                        atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){
                    System.out.println("11111111");
                    break;
                }
            }
        },"線程1:").start();

        new Thread(() -> {
            while(true){
                if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                        atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){
                    System.out.println("2222222");
                    break;
                }
            }
        },"線程2:").start();


        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(atoReferenceCount.getReference());
    }

第二個沒有執行,因為時間戳不對了。

修改下代碼:

 @Test
    public void test4() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                        atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(),
                        atoReferenceCount.getStamp() + 1);

                System.out.println("線程"+Thread.currentThread()+"result="+f);
            }, "線程:"+i).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(atoReferenceCount.getReference());
    }

結果:可見線程:0,比較的時候發現時間戳變了,所以沒有+1。

demo2:

@Test
    public void test5() {
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                for (int j = 0; j < 500; j++) {
                    boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                            atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(),
                            atoReferenceCount.getStamp() + 1);

                    System.out.println("線程"+Thread.currentThread()+">>j="+j+",result="+f);
                }
            }, "線程:"+i).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(atoReferenceCount.getReference());
    }

 有3次比較時間戳發現已經不同

 

參考:

https://blog.csdn.net/qq_32998153/article/details/79529704


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM