多線程之美6一CAS與自旋鎖

一、什麼是CAS

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

爲何預期值A與內存值V不同了呢?數據庫

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

cas缺點:多線程

一、存在ABA問題併發

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

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

二、原子類 Atomic

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

類圖結構以下:this

分別用AtomicInteger和 Integer 演示多個線程執行自增操做,是否可以保證原子性,執行結果是否正確atom

代碼以下:

/**
 * @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);

三、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

四、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

五、利用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);
    }
}

執行結果以下圖:

六、總結

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

相關文章
相關標籤/搜索