1. 程式人生 > >CAS原子操作

CAS原子操作

原子操作

所謂原子操作是指不會被執行緒排程機制打斷的操作,當某次操作一旦開始,就一直執行到結束,中間不會有任何中斷。
舉個例子:

A想要從自己的帳戶中轉1000塊錢到B的帳戶裡。那個從A開始轉帳,到轉帳結束的這一個過程,稱之為一個事務。在這個事務裡,要做如下操作:

  1. 從A的帳戶中減去1000塊錢。如果A的帳戶原來有3000塊錢,現在就變成2000塊錢了。
  2. 在B的帳戶里加1000塊錢。如果B的帳戶如果原來有2000塊錢,現在則變成3000塊錢了。
    如果在A的帳戶已經減去了1000塊錢的時候,忽然發生了意外,比如停電什麼的,導致轉帳事務意外終止了,而此時B的帳戶裡還沒有增加1000塊錢。那麼,我們稱這個操作失敗了,要進行回滾。回滾就是回到事務開始之前的狀態,也就是回到A的帳戶還沒減1000塊的狀態,B的帳戶的原來的狀態。此時A的帳戶仍然有3000塊,B的帳戶仍然有2000塊。

我們把這種要麼一起成功(A帳戶成功減少1000,同時B帳戶成功增加1000),要麼一起失敗(A帳戶回到原來狀態,B帳戶也回到原來狀態)的操作叫原子性操作。

如果把一個事務可看作是一個程式,它要麼完整的被執行,要麼完全不執行。這種特性就叫原子性。

CAS

Compare And Set(或Compare And Swap),CAS是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,採用這種無鎖的原子操作可以實現執行緒安全,避免加鎖的笨重性。
CAS操作包含三個運算元:記憶體位置(V)、預期原值(A)、新值(B)。
如果記憶體位置的值與預期原值相等,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。
無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”
在java中可以通過迴圈CAS的方式來實現原子操作。

CAS是實現自旋鎖的基礎,CAS 利用 CPU 指令保證了操作的原子性,以達到鎖的效果,迴圈這個指令,直到成功為止。
java提供的CAS原子操作類AtomicInteger等,核心就是CAS(CompareAndSwap)。

注意:原子操作和鎖是一樣的一種可以保證執行緒安全的方式,如何讓執行緒安全就看如何使用鎖或者如何使用原子操作CAS使用了正確的原子操作,所以保證了執行緒安全。

原子操作類

當高併發的情況下,對於基本資料型別或者引用資料型別的操作,可能會產生執行緒安全問題,為了避免多執行緒問題的處理方式一般有加鎖,但是加鎖會影響效能,所以這個時候可以考慮使用原子操作類。CAS由於是在硬體方面保證的原子性,不會鎖住當前執行緒,所以執行效率是很高的。
常見的原子操作類:
在這裡插入圖片描述

實戰

1,多執行緒累加一個較大的數值,比較原子操作類和加鎖各自的耗時。

public class AtomicIntegerDemo {
    
    static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) throws InterruptedException {
//        test();
        Synch synch = new Synch();
        synch.start();
        synch.join();
        atomic();

//        System.out.println(add());
    }

    //使用加鎖累加
    static class Synch extends Thread {
        @Override
        public void run() {
            //使用原子操作類統計
            long startTime = System.currentTimeMillis();
            int count = 1;
//            synchronized (this) {
//                for (int i = 0; i < 1000000000; i++) {
//                    count++;
//                }
//            }
            for (int i = 0; i < 1000000000; i++) {
                synchronized (this) {
                    count++;
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("使用鎖累加花費的時間:" + (endTime - startTime) + "......count = " + count);
        }
    }


    //使用原子操作類統計
    private static void atomic() {
        long startTime = System.currentTimeMillis();
        AtomicInteger atomicInteger = new AtomicInteger(1);
        for (int i = 0; i < 1000000000; i++) {
            atomicInteger.incrementAndGet();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("原子操作類累加花費的時間:" + (endTime - startTime) + "......count = " + atomicInteger.get());
    }

    //測試基本用法
    private static void test() {
        AtomicInteger num = new AtomicInteger(1);

        //++i
        System.out.println(num.incrementAndGet());

        //i++
        System.out.println(num.getAndIncrement());

        //CAS
        System.out.println(num.compareAndSet(3, 4));
        System.out.println(num.get());
    }

    //實現自定義的原子遞增方法
    private static int add() {
        for (; ; ) {
            int i = atomicInteger.get();
            boolean b = atomicInteger.compareAndSet(i, i + 1);
            if (b) {
                return atomicInteger.get();
            }
        }
    }
}

執行結果:
在這裡插入圖片描述

2,使用AtomicInteger類的get方法和compareAndSet方法實現它的遞增方法。

public class HalfAtomicInt {
    private AtomicInteger atomicI = new AtomicInteger(0);

    public void increament() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }
    
    public int getCount() {
    	return atomicI.get();
    }

}

總結

好處:保證了資料的原子性,避免執行緒安全問題,替代加鎖的效能消耗。
壞處:
1,ABA問題(併發1在修改資料時,雖然還是A,但已經不是初始條件的A了,中間發生了A變B,B又變A的變化,此A已經非彼A,資料卻成功修改,可能導致錯誤,這就是CAS引發的所謂的ABA問題。 可以使用樂觀鎖的方式解決。)
2,迴圈時間長開銷大(自旋)
3,只能保證一個共享變數的原子操作(可以使用AtomicRefrence原子操作類將多個變數合併成一個物件來解決)
解決ABA問題:
AtomicMarkableReference:內部是一個boolean型別的版本號,可以記錄是否被更改過。
AtomicStampedReference:內部是一個int型別的版本號,可以記錄被更改的次數。

例如:使用AtomicStampedReference,避免ABA問題,檢視內部是int型別的版本號。

public class AtomicStampedReferenceDemo {

    public static void main(String[] args) throws InterruptedException {
        //設定初始化版本號是0
        AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference("a1", 0);

        //初始的值和版本號
        String reference = atomicStampedReference.getReference();
        int stamp = atomicStampedReference.getStamp();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("目前的值:" + reference + "............版本號:" + stamp
                        + ",修改結果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = atomicStampedReference.getReference();
                System.out.println("目前的值:" + reference + "............版本號:" + atomicStampedReference.getStamp()
                        + ",修改結果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
            }
        });

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
    }

}

執行結果:
在這裡插入圖片描述