java原子操作CAS


  本次內容主要講原子操作的概念、原子操作的實現方式、CAS的使用、原理、3大問題及其解決方案,最后還講到了JDK中經常使用到的原子操作類。

 

1、什么是原子操作?

   所謂原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分。我們常用的i++看起來雖然簡單,但這並不是一個原子操作,具體原理后面單獨介紹。假定有兩個操作AB,如果從執行A的線程來看,當另一個線程執行B時,要么將B全部執行完,要么完全不執行B,那么AB對彼此來說是原子的。將整個操作視作一個整體是原子性的核心特征。

2、如何實現原子操作?

2.1 鎖機制實現原子操作及其問題

實現原子操作可以使用鎖。鎖機制滿足基本的需求是沒有問題的,但是有的時候我們的需求並非這么簡單,我們需要更有效,更加靈活的機制。synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖。使用synchronized關鍵字存在這樣的問題:

(1)如果被阻塞的線程優先級很高很重要怎么辦?

(2)如果獲得鎖的線程一直不釋放鎖怎么辦?

(3)如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭,同時,還有可能出現一些例如死鎖之類的情況。

使用鎖機制是一種比較粗糙、粒度比較大的機制,我們可以想象多個線程操作同一個計數器的業務場景,使用鎖機制的話顯得太過笨重。

2.2 CAS機制

  實現原子操作還可以使用當前的處理器基本都支持CAS(Compare And Swap)的指令,CPU指令集上提供了CAS操作相關指令,實現原子操作可以使用這些指令。每一個CAS操作過程都包含3個運算參數:一個內存地址V,一個期望的值A和一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦為新值B,否則不做任何操作

2.3 CAS使用

  先來模擬一個多個線程操作同一個計數器的場景,JDK中提供了boolean、int和long基本類型對應的原子包裝類AtomicBoolean、AtomicIntegerAtomicLong我們用AtomicInteger演示,通過CountDownLatch進行並發模擬,如果對CountDownLatch用法不了解,歡迎查看上一篇文章,有通俗易懂的例子。先對AtomicInteger的主要API做一個介紹:

(1)int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger里的value)相加,並返回結果。

(2)boolean compareAndSet(int expect,int update):如果當前數值等於expect,則以原子方式將當前值設置為update。

(3)int getAndIncrement():以原子方式將當前值加1,注意,這里返回的是自增前的值。

(4)int getAndSet(int newValue):以原子方式設置為newValue的值,並返回舊值。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    static AtomicInteger counter = new AtomicInteger(0);

    static CountDownLatch countDownLatch = new CountDownLatch(20);

    static class CounterThread implements Runnable {
        @Override
        public void run() {
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            counter.getAndIncrement();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Runnable thread = new CounterThread();
            new Thread(thread).start();
            countDownLatch.countDown();
        }
        Thread.sleep(2000); //保證子線程全部執行完成
        System.out.println("20個線程並發執行getAndIncrement()方法后的結果:" + counter.get());
        counter.compareAndSet(20, 18);//如果counter當前數值為20,則以原子方式更新為18
        System.out.println("compareAndSet(20, 18)后的結果:" + counter.get());
    }
}

程序中模擬了20個線程並發對一個計數器進行自增操作,結果輸出為20,可以看到這段代碼並沒有用任何的鎖,也達到了原子操作目的。

2.4 CAS原理

  CAS的基本思路就是,如果內存地址V上的值和期望的值A相等,則給其賦予新值B,否則不做任何事兒。CAS就是在一個循環里不斷的做CAS操作,直到成功為止。CAS是怎么實現線程的安全呢?語言層面不做處理,JDK 調用這些指令來完成CAS操作,本質上就是將其交給CPU和內存,利用CPU的多處理能力,實現硬件層面的阻塞,再加上volatile變量的特性即可實現基於原子操作的線程安全。用一張圖來說明。

3、CAS實現原子操作的三大問題

 3.1 ABA問題

  因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。舉個通俗易懂的例子,我的同事老王今年35歲了,還沒有女朋友,我問他有什么要求,給他介紹一個女朋友。老王就說了,只要是沒有結婚、35歲以下的女的就行。於是我就給他介紹了一個28歲,剛剛離婚不久的女同志,他還感謝了我好久,可能是他現在都還不知道他這個女朋友離過婚。這就是典型的ABA問題,只關心當前狀態,而不管中間經歷了什么。ABA問題的解決思路就是使用版本號。給變量追加一個版本號,每次變量更新的時候把版本號加1,那么A→B→A就會變成1A→2B→3A。就好比老王的要求改成:35歲以下,沒有結婚並且離婚次數為0的女性,就不會發生剛剛的事情了。

 3.2 循環時間長開銷大。

   CAS自旋如果長時間不成功,會給CPU帶來非常大的執行開銷。

3.3 只能保證一個共享變量的原子操作

  當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。怎么解決這個問題呢?從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。

4、JDK中相關原子操作類

4.1 AtomicReference

  AtomicReference,可以原子更新的對象引用。AtomicReference有一個compareAndSet()方法,它可以將已持有引用與預期引用進行比較,如果它們相等,則在AtomicReference對象內設置一個新的引用。看一段代碼:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    static AtomicReference<UserInfo> atomicReference;

    public static void main(String[] args) {
        //原引用
        UserInfo oldUser = new UserInfo("老王", 35);
        atomicReference = new AtomicReference<>(oldUser);

        //新引用
        UserInfo updateUser = new UserInfo("小宋", 21);
        atomicReference.compareAndSet(oldUser, updateUser);

        System.out.println("使用compareAndSet()替換原有引用后的結果:" + atomicReference.get());
        System.out.println("原引用:" + oldUser);
    }

    static class UserInfo {
        private String name;
        private int age;

        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

從程序輸出可以看到,atomicReference的持有的引用被修改了,但是原引用對象並沒有發生改變。

 

4.2 AtomicStampedReference

   AtomicStampedReference,利用版本戳的形式記錄了每次改變以后的版本號,這樣的話就不會存在ABA問題了。 AtomicStampedReference有一個內部類Pair,使用Pair的int stamp作為計數器使用,看下Pair的源碼:

 private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

還是老王那個例子,如果使用AtomicStampedReference的話,老王更關心的是介紹的女朋友離過幾次婚。用一段代碼來模擬給老王介紹女朋友的場景:

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    static AtomicStampedReference<String> asr = new AtomicStampedReference("介紹的女朋友", 0);

    public static void main(String[] args) throws InterruptedException {
        final String oldReference = asr.getReference();//初始值,表示介紹的女朋友
        final int oldStamp = asr.getStamp();//初始版本0,表示介紹的女朋友沒有離過婚

        Thread thread1 = new Thread(() -> {
            String newReference = oldReference + " 離婚1次";
            boolean first = asr.compareAndSet(oldReference, newReference,
                    oldStamp, oldStamp + 1);
            if (first) {
                System.out.println("介紹的女朋友第一次離婚。。。");
            }

            boolean second = asr.compareAndSet(newReference, oldReference + "又離婚了",
                    oldStamp + 1, oldStamp + 2);
            if (second) {
                System.out.println("介紹的女朋友第二次離婚。。。");
            }
        }, "介紹的女朋友離婚");

        Thread thread2 = new Thread(() -> {
            String reference = asr.getReference();//介紹的女朋友最新狀態

            //判斷介紹的女朋友最新狀態是否符合老王的要求
            boolean flag = asr.compareAndSet(reference, reference + "沒有離過婚",
                    oldStamp, oldStamp + 1);
            if (flag) {
                System.out.println("老王笑嘻嘻地對我說,介紹的女朋友符合我的要求");
            } else {
                System.out.println("老王拳頭緊握地對我說,介紹的女朋友居然離過" + asr.getStamp() + "次婚,不符合我要求!!!!");
            }
        }, "老王相親");
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
    }
}

 啟動2個子線程,分別代表介紹的女朋友多次離婚以及老王相親的場景。從程序輸出可以看到,介紹的女朋友不符合老王的要求,老王為了避免喜當爹,果斷拒絕了。

 老王判斷的依據是,介紹的女朋友應該是沒有離過婚,stamp值等於0才對。但是老王仔細一看,stamp已經是2,不符合我的要求,不能要。

4.3 AtomicMarkableReference

   AtomicMarkableReference,可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。AtomicMarkableReference也有一個內部類Pair,使用Pair的boolean mark來標記狀態。還是老王那個例子,使用AtomicStampedReference可能關心的是離婚次數,AtomicMarkableReference關心的是有沒有離過婚。用一段代碼來模擬:

import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceDemo {
    static AtomicMarkableReference markableReference;

    public static void main(String[] args) throws InterruptedException {
        String girl = "介紹的女朋友";
        markableReference = new AtomicMarkableReference(girl, false);
        Thread t1 = new Thread(() -> {
            markableReference.compareAndSet(girl, girl + "離婚", false, true);
            System.out.println(markableReference.getReference());
        }, "介紹的女朋友離婚了");

        Thread t2 = new Thread(() -> {
            //老王檢查標記,只關心這個標志位
            boolean marked = markableReference.isMarked();
            if (marked) {
                System.out.println("你給我介紹的女朋友離過婚,我不要!!");
            } else {
                System.out.println("兄弟,大兄弟,親生兄弟啊!!這個女朋友我要了");
            }
        }, "老王鑒定介紹的女朋友有沒有離過婚");

        t1.start();
        t1.join();

        t2.start();
        t2.join();
    }
}

程序輸出可以看到,老王還是堅持了自己的原則。

4.4 AtomicIntegerArray

   AtomicIntegerArray,元素可以原子更新的數組。其常用方法如下:

(1)int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。

(2)boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

需要注意的是,數組value通過構造方法傳遞進去,然后AtomicIntegerArray會將當前數組復制一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。

用法比較簡單,看一個例子:

public class AtomicIntegerArrayDemo {
    static int[] value = new int[]{1, 2};//原始數組
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        atomicIntegerArray.getAndSet(0, 3);
        System.out.println("atomicIntegerArray的第一個元素:" + atomicIntegerArray.get(0));
        System.out.println("原始數組的第一個元素:" + value[0]);//原數組不會變化
    }
}

程序輸出可以看到,原始數組並沒有受到影響。

 順便看一下AtomicIntegerArray的構造方法:

 public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    }

 5、結語

  文中例子純屬虛構,便於對知識點的理解,不摻雜任何其他意思。下一篇內容中會介紹Java的顯示鎖Lock相關知識點,閱讀過程中如發現描述有誤,請指出,謝謝。

 

 


免責聲明!

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



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