多線程6一CAS與自旋鎖

1、什麼是CAS

CAS 即 compare and swap 比較並交換, 涉及到三個參數,內存值V, 預期值A, 要更新爲的值B, 拿着預期值A與內存值V比較,相等則符合預期,將內存值V更新爲B, 不相等,則不能更新V。

爲什麼預期值A與內存值V不一樣了呢?

在多線程環境下,對於臨界區的共享資源,所有線程都可以訪問修改,這時爲了保證數據不會發生錯誤,通常會對訪問臨界區資源加鎖,同一時刻最多隻能讓一個線程訪問(獨佔模式下),這樣會讓線程到臨界區時串行執行,加鎖操作可能會導致併發性能降低,而循環CAS可以實現讓多個線程不加鎖去訪問共享資源,卻也可以保證數據正確性。 如 int share = 1,線程A獲取到share的值1,想要將其修改爲2,這時線程B搶先修改share = 3了,線程A這時拿着share =1 預期值與實際內存中已經變爲3的值比較, 不相等,cas失敗,這時就重新獲取最新的share再次更新,需要不斷循環,直到更新成功;這裏可能會存在線程一直在進行循環cas,消耗cpu資源。

cas缺點:

1、存在ABA問題

2、循環cas, 可能會花費大量時間在循環,浪費cpu資源

3、只能更新一個值(也可解決,AtomicReference 原子引用類泛型可指定對象,實現一個對象中包含多個屬性值來解決只能更新一個值的問題)

2、原子類 Atomic

原子類在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本數據類型原子類,還有可傳泛型的AtomicReference, 以及帶有版本號的 AtomicStampedReference , 可實現對象的原子更新, 其具體是怎樣保證在多線程環境下,不加鎖的情況也可以原子操作, 是其內部藉助了Unsafe類,來保證更新的原子性。

類圖結構如下:在這裏插入圖片描述
分別用AtomicInteger和 Integer 演示多個線程執行自增操作,是否能夠保證原子性,執行結果是否正確

代碼如下:

/**
 * @author zdd
 * 2019/12/22 10:47 上午
 * Description: 演示AtomicInteger原子類原子操作
 */
public class CasAtomicIntegerTest {
    static  final Integer THREAD_NUMBER = 10;
    static  AtomicInteger atomicInteger = new AtomicInteger(0);
    static  volatile Integer integer = 0;

    public static void main(String[] args) throws InterruptedException {
        ThreadTask task = new ThreadTask();
        Thread[] threads = new Thread[THREAD_NUMBER];
        //1,開啓10個線程
        for (int j = 0; j < THREAD_NUMBER; j++) {
            Thread thread  = new Thread(task);
            threads[j]= thread;
        }
        for (Thread thread:threads) {
            //開啓線程
            thread.start();
            //注: join 爲了保證主線程在所有子線程執行完畢後再打印結果,否則主線程就阻塞等待
           // thread.join();
        }

        // 主線程休眠5s, 等待所有子線程執行完畢再打印
        TimeUnit.SECONDS.sleep(5);

        System.out.println("執行完畢,atomicInteger的值爲: "+ atomicInteger.get());
        System.out.println("執行完畢,integer的值爲 : "+ integer);
    }

    public static void  safeIncr() {
        atomicInteger.incrementAndGet();
    }
    public static void  unSafeIncr() {
        integer ++;
    }

    static class ThreadTask implements  Runnable{
        @Override
        public void run() {
            // 任務體,分別安全和非安全方式自增1000次
            for (int i = 0; i < 1000; i++) {
                safeIncr();
            }
            for (int i = 0; i < 1000; i++) {
                unSafeIncr();
            }
        }
    }
}

執行結果如下:
在這裏插入圖片描述

疑問: 上文代碼中注,我本想讓主線程調用每個子線程 join方法,保證主線程在所有子線程執行完畢之後再執行打印結果,然而這樣執行導致非安全的Integer自增結果也正確,猜想是在執行join方法,導致這10個子線程排隊有序在執行了? 因此註釋了該行代碼 ,改爲讓主線程休眠幾秒來保證在子線程執行後再打印。

AtomicInteger如何保證原子性,AtomicInteger持有Unsafe對象,其大部分方法是本地方法,底層實現可保證原子操作。

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();

來看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加後的值。

代碼如下:

  public final int incrementAndGet() {
       //調用unsafe的方法
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

繼續看unsafe如何實現

  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
          //1.獲取當前對象的內存中的值A
            var5 = this.getIntVolatile(var1, var2);
          //2. var1,var2聯合獲取內存中的值V,var5是期望中的值A, var5+var4 是將要更新爲的新值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
       //3. 更新成功,跳出while循環,返回更新成功時內存中的值(可能下一刻就被其他線程修改)
        return var5;
    }

執行流程圖如下:
在這裏插入圖片描述Unsafe 的compareAndSwapInt是本地方法,可原子地執行更新操作,更新成功返回true,否則false

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

3、CAS的ABA問題

什麼是ABA問題?

例如 線程A獲取變量atomicInteger =100, 想要將其修改爲2019 (此時還未修改), 這時線程B搶先進來將atomicInteger先修改爲101,再修改回atomicInteger =100,這時線程A開始去更新atomicInteger的值了,此時預期值和內存值相等,更新成功atomicInteger =2019;但是線程A 並不知道這個值其實已經被人修改過了。

代碼演示如下:

/**
 * zdd
 * Description: cas的ABA問題
 */
public class CasTest1 {

   // static AtomicInteger atomicInteger = new AtomicInteger(100);
   /* 這裏使用原子引用類,傳入Integer類型,
    * 和AtomicInteger一樣,AtomicReference使用更靈活,泛型可指定任何引用類型。
    * 也可用上面註釋代碼
    */
    static AtomicReference<Integer>  reference = new AtomicReference<>(100);

    public static void main(String[] args) {
  
      //1.開啓線程A
        new Thread(()-> {
            Integer expect =  reference.get();
            try {
                //模擬執行任務,讓線程B搶先修改
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( "執行3s任務後, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ "  當前值爲: "+ reference.get());
        },"A").start();     
    //2.開啓線程B
        new Thread(()-> {
            // expect1 =100
            Integer expect1 =  reference.get();
            //1,先修改爲101,再修改回100,產生ABA問題
            reference.compareAndSet(expect1,101);
            //expect2 =101
            Integer expect2 =  reference.get();
            reference.compareAndSet(expect2, 100);
        },"B").start();

    }
}      

執行結果如下:可見線程A修改成功

A 執行3s任務後, 修改值是否成功:true  當前值爲: 2019

4、ABA問題的解決方式

解決CAS的ABA問題,是參照數據庫樂觀鎖,添加一個版本號,每更新一次,次數+1,就可解決ABA問題了。

AtomicStampedReference

/**
 * zdd
 * 2019/11/4 6:30 下午
 * Description:
 */
public class CasTest1 {
  //設置初始值和版本號
    static  AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        //2,採用帶有版本號的 
        new Thread(()-> {
            Integer  expect = stampedReference.getReference();
            int     stamp = stampedReference.getStamp();
            try {
                //休眠3s,讓線程B執行完ABA操作
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //此時 stamp=1,與實際版本號3不等,這裏更新失敗就是stamp沒有獲取到最新的
            System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
            System.out.println("當前 stamp 值: " + stampedReference.getStamp()+ "當前 reference: " +stampedReference.getReference());

        },"A").start();

        new Thread(()-> {
            Integer expect = stampedReference.getReference();
            int stamp = stampedReference.getStamp();
            try {
                //休眠1s,讓線程A獲取都舊的值和版本號
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 1,100 -> 101, 版本號 1-> 2
            stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
            //2, 101 ->100, 版本號 2->3
            Integer expect2 = stampedReference.getReference();
            stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);

        },"B").start();
    }
}

執行結果如下:

是否修改成功: false
當前 stamp 值: 3  當前 reference: 100

5、利用cas實現自旋鎖

package cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author zdd
 * 2019/12/22 9:12 下午
 * Description: 利用cas手動實現自旋鎖
 */
public class SpinLockTest {

    static   AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public static void main(String[] args) {
        SpinLockTest spinLockTest = new SpinLockTest();
        //測試使用自旋鎖,達到同步鎖一樣的效果 ,開啓2個子線程
        new Thread(()-> {
            spinLockTest.lock();
            System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
            try {
                //休眠3s
                TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
            spinLockTest.unLock();
        },"線程A").start();

        new Thread(()-> {
            spinLockTest.lock();
            System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
            try {
                //休眠3s
                TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
            spinLockTest.unLock();
        },"線程B").start();
    }
    public static void lock() {
      Thread currentThread =  Thread.currentThread();
      for (;;) {
          boolean flag =atomicReference.compareAndSet(null,currentThread);
         //cas更新成功,則跳出循環,否則一直輪詢
          if(flag) {
              break;
          }
      }
    }
    public static void unLock() {
        Thread currentThread = Thread.currentThread();
        Thread momeryThread  = atomicReference.get();
        //比較內存中線程對象與當前對象,不等拋出異常,防止未獲取到鎖的線程調用unlock
        if(currentThread != momeryThread) {
            throw new IllegalMonitorStateException();
        }
        //釋放鎖
        atomicReference.compareAndSet(currentThread,null);
    }
}

執行結果如下圖:
在這裏插入圖片描述

6、總結

通過全文,我們可以知道cas的概念,它的優缺點;原子類的使用,內部藉助Unsafe類循環cas更新操作實現無鎖情況下保證原子更新操作,進一步我們能夠自己利用循環cas實現自旋鎖SpinLock,它與同步鎖如ReentrantLock等區別在於自旋鎖是在未獲取到鎖情況,一直在輪詢,線程時非阻塞的,對cpu資源佔用大,適合查詢多修改少場景,併發性能高;同步鎖是未獲取到鎖,阻塞等待,兩者各有適用場景。


道阻且長,且歌且行!

每天一小步,踏踏實實走好腳下的路,文章爲自己學習總結,不復制黏貼,就是想讓自己的知識沉澱一下,也希望與更多的人交流,如有錯誤,請批評指正!

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