Java原子操做類,你知道多少?

原子操做類簡介
因爲synchronized是採用的是悲觀鎖策略,並非特別高效的一種解決方案。 實際上,在J.U.C下的atomic包提供了一系列的操做簡單,性能高效,並能保證線程安全的類去 更新基本類型變量,數組元素,引用類型以及更新對象中的字段類型。 atomic包下的這些類都是採用的是樂觀鎖策略去原子更新數據,在Java中則是使用CAS操做具體實現。java

CAS
隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略: 先進行操做,若是沒有其它線程爭用共享數據,那操做就成功了,不然採起補償措施(不斷地重試,直到成功爲止)。 這種樂觀的併發策略的許多實現都不須要將線程阻塞,所以這種同步操做稱爲非阻塞同步。 樂觀鎖須要操做和衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。 硬件支持的原子性操做最典型的是:比較並交換(Compare-and-Swap,CAS)。 CAS 指令須要有 3 個操做數,分別是內存地址 V、舊的預期值 A 和新值 B。 當執行操做時,只有當 V 的值等於 A,纔將 V 的值更新爲 B。數組

//著名的CAS
//var1是比較值所屬的對象,var2須要比較的值(但實際是使用地址偏移量來實現的),
//若是var1對象中偏移量爲var2處的值等於var4,那麼將該處的值設置爲var5並返回true,若是不等於var4則返回false。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS的問題
1.ABA問題安全

若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。多線程

J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題, 它能夠經過控制變量值的版原本保證 CAS 的正確性。 大部分狀況下 ABA 問題不會影響程序併發的正確性, 若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。併發

2.自旋時間長開銷大分佈式

自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。 若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用, 第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源, 延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。 第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation) 而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。ide

3.只能保證一個共享變量的原子操做 CAS只對單個共享變量有效,當操做涉及跨多個共享變量時CAS無效。 可是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性, 能夠把多個變量封裝成對象裏來進行 CAS 操做. 因此咱們可使用鎖或者利用AtomicReference類把多個共享變量封裝成一個共享變量來操做。高併發

synchronized VS CAS
元老級的synchronized(未優化前)最主要的問題是: 在存在線程競爭的狀況下會出現線程阻塞和喚醒鎖帶來的性能問題,由於這是一種互斥同步(阻塞同步)。 而CAS並非武斷的將線程掛起,當CAS操做失敗後會進行必定的嘗試,而不是進行耗時的掛起喚醒的操做, 所以也叫作非阻塞同步。這是二者主要的區別。工具

原子更新基本類
atomic包提升原子更新基本類的工具類,以下:性能

AtomicBoolean //以原子更新的方式更新Boolean

AtomicIntege //以原子更新的方式更新Integer

AtomicLong //以原子更新的方式更新Long
以AtomicInteger爲例總結經常使用的方法:
addAndGet(int delta) //以原子方式將輸入的數值與實例中本來的值相加,並返回最後的結果

incrementAndGet() //以原子的方式將實例中的原值進行加1操做,並返回最終相加後的結果

getAndSet(int newValue) //將實例中的值更新爲新值,並返回舊值

getAndIncrement() //以原子的方式將實例中的原值加1,返回的是自增前的舊值
AtomicInteger的getAndIncrement()方法源碼以下:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

其實是調用了unsafe實例的getAndAddInt方法,unsafe實例的獲取時經過UnSafe類的靜態方法getUnsafe獲取:

private static final Unsafe unsafe = Unsafe.getUnsafe();
public class AtomicIntegerDemo {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;

    //java.util.concurrent.atomic.AtomicInteger;
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Semaphore和CountDownLatch模擬併發
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("count:{"+count.get()+"}");
    }

    public static void add() {
        count.incrementAndGet();
    }
}

輸出結果:

count:{5000}

AtomicLong的實現原理和AtomicInteger一致,只不過一個針對的是long變量,一個針對的是int變量。 而boolean變量的更新類AtomicBoolean類是怎樣實現更新的呢?核心方法是compareAndSet()方法,其源碼以下:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

能夠看出,compareAndSet方法的實際上也是先轉換成0,1的整型變量, 而後是經過針對int型變量的原子更新方法compareAndSwapInt來實現的。 能夠看出atomic包中只提供了對boolean,int ,long這三種基本類型的原子更新的方法, 參考對boolean更新的方式,原子更新char,doule,float也能夠採用相似的思路進行實現。

原子更新數組
atomic包下提供能原子更新數組中元素的類有:

AtomicIntegerArray //原子更新整型數組中的元素

AtomicLongArray //原子更新長整型數組中的元素

AtomicReferenceArray //原子更新引用類型數組中的元素
這幾個類的用法一致,就以AtomicIntegerArray來總結下經常使用的方法:

getAndAdd(int i, int delta) //以原子更新的方式將數組中索引爲i的元素與輸入值相加

getAndIncrement(int i) //以原子更新的方式將數組中索引爲i的元素自增長1

compareAndSet(int i, int expect, int update) //將數組中索引爲i的位置的元素進行更新
能夠看出,AtomicIntegerArray與AtomicInteger的方法基本一致, 只不過在AtomicIntegerArray的方法中會多一個指定數組索引位i。

public class AtomicIntegerArrayDemo {
    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對數組中索引爲1的位置的元素加5
        int result = integerArray.getAndAdd(1, 5);
        System.out.println(integerArray.get(1));
        System.out.println(result);
    }
}

輸出結果:

7
2

原子更新引用類型
若是須要原子更新引用類型變量的話,爲了保證線程安全,atomic也提供了相關的類:

AtomicReference //原子更新引用類型

AtomicReferenceFieldUpdater //原子更新引用類型裏的字段

AtomicMarkableReference //原子更新帶有標記位的引用類型
這幾個類的使用方法也是基本同樣的,以AtomicReference爲例。

public class AtomicReferenceDemo {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

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

輸出結果:

User{userName='a', age=1}
User{userName='b', age=2}

首先將對象User1用AtomicReference進行封裝,而後調用getAndSet方法, 從結果能夠看出,該方法會原子更新引用的user對象, 變爲User{userName='b', age=2},返回的是原來的user對象User{userName='a', age=1}。

原子更新字段類型
若是須要更新對象的某個字段,並在多線程的狀況下,可以保證線程安全,atomic一樣也提供了相應的原子操做類:

AtomicIntegeFieldUpdater //原子更新整型字段類

AtomicLongFieldUpdater //原子更新長整型字段類

AtomicStampedReference //原子更新引用類型,這種更新方式會帶有版本號。
// 而爲何在更新的時候會帶有版本號,是爲了解決CAS的ABA問題;
要想使用原子更新字段須要兩步操做:

原子更新字段類都是抽象類,只能經過靜態方法newUpdater來建立一個更新器,而且須要設置想要更新的類和屬性
更新類的屬性必須使用public volatile進行修飾
這幾個類提供的方法基本一致,以AtomicIntegerFieldUpdater爲例。

public class AtomicIntegerFieldUpdaterDemo {
    private static AtomicIntegerFieldUpdater updater =
        AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    
    public static void main(String[] args) {
        User user = new User("a", 1);
        int oldValue = updater.getAndAdd(user, 5);
        System.out.println(oldValue);
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

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

輸出結果:

1
6

建立AtomicIntegerFieldUpdater是經過它提供的靜態方法進行建立, getAndAdd方法會將指定的字段加上輸入的值,而且返回相加以前的值。 user對象中age字段原值爲1,加5以後,能夠看出user對象中的age字段的值已經變成了6。

免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門:https://mp.weixin.qq.com/s/Jz...

相關文章
相關標籤/搜索