帶你深刻理解原子類

1.什麼是原子類

原子的大概意思就是不可分割,在程序中的表現形式爲一個操做 不可中斷,即使是多線程的狀況下也能夠保證,其中在 Java中就有 java.util.concurrent.atomic包,這個包用來存儲原子類。

2.原子類的做用

原子類的做用與鎖相似,都是爲了在併發場景下保證 線程安全,不過原子類相對於鎖具備如下優點:
  • 鎖的粒度更細:原子變量能夠把競爭範圍縮小到變量級別,這是咱們能夠得到的最細粒度的狀況了,一般鎖的粒度都會大於原子變量。
  • 效率較高:一般,使用原子類的效率會比使用鎖的效率更高,可是高度競爭的狀況除外。

3.原子類縱覽

4.演示原子類

4.1 經常使用方法(以AtomicInteger爲例)

  • public final int get(); // 獲取當前的值
  • public final int getAndSet(int newValue); // 獲取當前的值,並設置新值
  • public final int getAndIncrement(); // 獲取當前的值並自增
  • public final int getAndDecrement(); // 獲取當前的值並自減
  • public final int getAndAdd(int delta); // 獲取當前的值,並加上預期的值
  • boolean compareAndSet(int expect, int update); // 若是輸入的數值和預期相等,則以原子的方式將該值設置爲輸入值(update),這個方法也是CAS思想的體現。

4.2 用原子類與普通變量作對比

/**
 * 描述: 演示 AtomicInteger 的基本用法,而且對比非原子類的線程安全問題,
 * 使用了原子類以後,不須要加鎖也能夠保證線程安全。
 */
public class AtomicIntegerDemo1 implements Runnable {

    private static final AtomicInteger atomicInteger = new AtomicInteger();

    public void incrementAtomic() {
        atomicInteger.getAndIncrement();
    }

    // 添加 volatile 關鍵字保證可見性的同時不保證原子性
    private static volatile int basicCount = 0;

    public void incrementBasic() {
        basicCount++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            incrementAtomic();
            incrementBasic();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo1 demo1 = new AtomicIntegerDemo1();
        Thread t1 = new Thread(demo1);
        Thread t2 = new Thread(demo1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("原子類的結果是:" + atomicInteger.get());
        System.out.println("普通變量的結果是:" + basicCount);
    }
}

運行結果:java

4.3 使用原子數組

/**
 * 描述: 演示原子數組的使用方法
 */
public class AtomicArrayDemo {

    public static void main(String[] args) throws InterruptedException {

        AtomicIntegerArray atomicIntegerArray =
                new AtomicIntegerArray(1000);

        Decrementer decrementer = new Decrementer(atomicIntegerArray);
        Incrementer incrementer = new Incrementer(atomicIntegerArray);

        Thread[] threadsIncrementer = new Thread[100];
        Thread[] threadsDecrementer = new Thread[100];

        for (int i = 0; i < 100; i++) {
            threadsDecrementer[i] = new Thread(decrementer);
            threadsIncrementer[i] = new Thread(incrementer);

            threadsDecrementer[i].start();
            threadsIncrementer[i].start();
        }

        for (int i = 0; i < 100; i++) {
            threadsDecrementer[i].join();
            threadsIncrementer[i].join();
        }

        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            if (atomicIntegerArray.get(i) != 0) {
                System.out.println("發現了非0值" + i);
            }
        }
        System.out.println("運行結束");
    }

}

class Decrementer implements Runnable {

    private AtomicIntegerArray array;

    public Decrementer(AtomicIntegerArray array) {
        this.array = array;
    }

    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndDecrement(i);
        }
    }
}

class Incrementer implements Runnable {

    private AtomicIntegerArray array;

    public Incrementer(AtomicIntegerArray array) {
        this.array = array;
    }

    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndIncrement(i);
        }
    }
}

運行結果:數組


由於使用的是原子數組,因此不管運行多少次都不會出現非0的狀況。安全

4.4 Atomic引用在自旋鎖的應用

AtomicReference類的做用和 AtomicInteger 並無什麼區別AtomicInteger能夠 讓一個整數保證原子性,而 AtomicReference可讓一個 對象保證原子性。由於對象中能夠包含多個屬性,因此 AtomicReferenceAtomicInteger功能要強一些。
/**
 * 描述: 演示自旋鎖
 */
public class SpinLock {

    // 聲明自旋鎖
    private AtomicReference<Thread> sign = new AtomicReference<>();

    /**
     * 加鎖
     */
    public void lock() {
        Thread current = Thread.currentThread();
        // 使用 while 循環加 CAS 操做實現自旋
        // 但願沒有線程持有鎖,傳入 null
        // 但願更新的值是本身,傳入 current
        while (!sign.compareAndSet(null, current)) {
            System.out.println(Thread.currentThread().getName() + "自旋獲取失敗,再次嘗試");
        }
    }

    /**
     * 解鎖
     */
    public void unlock() {
        Thread current = Thread.currentThread();
        // 解鎖首先要有鎖,因此一次就能夠解掉,不須要 while 循環
        // 但願持有鎖的線程是本身,傳入 current
        // 由於是解鎖,因此要更新的值爲 null
        sign.compareAndSet(current, null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "開始嘗試獲取自旋鎖");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName()
                        + "獲取到了自旋鎖");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName()
                            + "釋放了自旋鎖");
                }
            }
        };

        // 用兩個線程執行任務,模擬爭搶
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

4.5 把普通變量升級爲原子變量

原子變量在保證線程安全的同時開銷也要比普通變量大得多,由於須要維護內部的一些邏輯,因此若是在須要的時候將普通變量升級爲原子變量的話,就能夠大大的節省系統資源,提高系統執行效率,下面就來演示一下將普通變量升級爲原子變量。
/**
 * 描述: 演示 FieldUpdater 的用法
 */
public class AtomicIntegerFieldUpdaterDemo implements Runnable {

    static Candidate tom;
    static Candidate peter;

    public static AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
            AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            peter.score++;
            // 經過 scoreUpdater 將普通變量升級爲原子變量
            scoreUpdater.getAndIncrement(tom);
        }
    }

    public static class Candidate {
        volatile int score;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
        tom = new Candidate();
        peter = new Candidate();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t1.join();
        System.out.println("普通的變量: " + peter.score);
        System.out.println("升級後的變量: " + tom.score);
    }
}

運行結果:多線程

4.5.1 使用屬性更新器的注意事項

  • 必須是int類型的字段,int的包裝類也不能夠
  • 必須添加volatile修飾
  • 不能是private訪問權限
  • 不能添加static關鍵字

4.6 Adder 累加器

因爲 long的字節數比 int要多,因此在使用 AtomicLong時的效率要比 AtomicInteger要低一些,針對這個狀況,在 Java 8 中引入了一個新的類 Adder累加器,在高併發場景下 LongAdder的開銷要比 AtomicLong效率要高,不過本質是 以空間換時間。當競爭激烈的時候, LongAdder把不一樣線程對應到不一樣的 Cell上進行修改,下降了衝突的機率,這也是 多段鎖的理念,從而提高了併發性。

4.6.1 演示 AtomicLong 與 LongAdder 的性能差異

  • AtomicLong
/**
 * 描述: 演示高併發場景下 LongAdder 要比 AtomicLong 要好。
 */
public class AtomicLongDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicLong counter = new AtomicLong(0);
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            executorService.submit(new Task(counter));
        }

        executorService.shutdown();
        while (!executorService.isTerminated()) {

        }
        long endTime = System.currentTimeMillis();
        System.out.println(counter.get());
        System.out.println("AtomicLong 耗時爲:" + (endTime - startTime) + " ms");
    }

    private static class Task implements Runnable {

        private AtomicLong counter;

        public Task(AtomicLong counter) {
            this.counter = counter;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet();
            }
        }
    }
}

運行結果:
併發

  • LongAdder
/**
 * 描述: 演示高併發場景下 LongAdder 要比 AtomicLong 要好。
 */
public class LongAdderDemo {
    public static void main(String[] args) throws InterruptedException {
        LongAdder counter = new LongAdder();
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            executorService.submit(new Task(counter));
        }

        executorService.shutdown();
        while (!executorService.isTerminated()) {

        }
        long endTime = System.currentTimeMillis();
        System.out.println(counter.sum());
        System.out.println("LongAdder 耗時爲:" + (endTime - startTime) + " ms");
    }

    private static class Task implements Runnable {

        private LongAdder counter;

        public Task(LongAdder counter) {
            this.counter = counter;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        }
    }
}

運行結果:ide

4.6.2 剖析高速運轉的原理

  • AtomicLong

    在使用AtomicLong進行累加時,每一個線程之間須要將使用共享內存進行通訊,即線程執行操做時只能在工做內存中進行,而後將值刷新到主內存,在高併發場景下頻繁的刷新和獲取操做會帶來必定的性能開銷。高併發

  • LongAdder
    LongAdder引入了分段累加的概念,在內部有一個base變量和Cell[]數組共同參與計數:工具

    • base變量:競爭不激烈,直接累加到該變量上。
    • Cell數組:競爭激烈,各個線程分散累加到本身的槽Cell[i]中。

4.6.3 sum 源碼分析

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

首先將負責計數的cells賦值給as,這樣就能夠經過as來獲取數值了,base用於併發不激烈的狀況下,這裏使用了sum進行統計。接下來是判斷as的是否爲空,若是爲空說明根本沒有向Cell中存儲數,因此直接返回sum(base)便可,若是用到了cell就將它作遍歷,並將值從中取出,並用sum進行統計。源碼分析

總結:在低爭用下,AtomicLongLongAdder這兩個類具備相同的特性。可是在競爭激烈的狀況下,LongAdder的預期吞吐量要高得多,可是消耗的空間也多。性能

LongAdder適合的場景是統計求和計數的場景,並且LongAdder基本只提供add()方法,而AtomicLong還具備CAS方法。

4.7 Accumulator 累加器

AccumulatorAdder很是類似, Accumulator是一個更通用版的 Adder

4.7.1 使用 LongAccumulator 累加器

/**
 * 描述: 演示 LongAccumulator 的用法
 */
public class LongAccumulatorDemo {
    public static void main(String[] args) {
        LongAccumulator longAccumulator =
                new LongAccumulator((x, y) -> x + y, 0);
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        // 從0開始,從1加到9
        IntStream.range(1, 10)
                .forEach(i -> executorService.submit(() -> longAccumulator.accumulate(i)));
        executorService.shutdown();
        while (!executorService.isTerminated()) {

        }
        System.out.println(longAccumulator.getThenReset());
    }
}

運行結果:

參考連接:

慕課網之玩轉Java併發工具,精通JUC,成爲併發多面手

相關文章
相關標籤/搜索