線程、多線程之Atomic 簡述

1. 介紹一下Atomic 原子類

        Atomic 翻譯成中文是原子的意思(事務的四個特性ACID,其中A就是原子性)。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裏 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。

併發包 java.util.concurrent 的原子類都存放在java.util.concurrent.atomic下,如下圖所示。

JUC原子類概覽

2. JUC 包中的原子類是哪4類?

2.1 基本類型

用原子的方式更新基本類型

  • tomicInteger:整形原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean:布爾型原子類

2.2 數組類型

使用原子的方式更新數組裏的某個元素

  • AtomicIntegerArray:整形數組原子類
  • AtomicLongArray:長整形數組原子類
  • AtomicReferenceArray:引用類型數組原子類

2.3 引用類型

  • AtomicReference:引用類型原子類
  • AtomicStampedReference:原子更新引用類型裏的字段原子類
  • AtomicMarkableReference :原子更新帶有標記位的引用類型

2.4 對象的屬性修改類型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新長整形字段的更新器
  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

3. 講講 AtomicInteger 的使用

3.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)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。Copy to clipboardErrorCopied

3.2 AtomicInteger 類的使用示例

使用 AtomicInteger 之後,不用對 increment() 方法加鎖也可以保證線程安全。

class AtomicIntegerTest {
        private AtomicInteger count = new AtomicInteger();
      //使用AtomicInteger之後,不需要對該方法加鎖,也可以實現線程安全。
        public void increment() {
                  count.incrementAndGet();
        }

       public int getCount() {
                return count.get();
        }
}

4. 能不能給我簡單介紹一下 AtomicInteger 類的原理

4.1 AtomicInteger 線程安全原理簡單分析

AtomicInteger 類的部分源碼:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較並替換”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

        AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大爲提升。

4.2 簡單瞭解下 CAS

       CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。

通俗一點來說,CAS的執行需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。

4.3 示例說明CAS

多線程的過程中對變量累加操作:

    public static volatile int count = 0;
    public static int STEP = 20;
    private static CountDownLatch countDownLatch = new CountDownLatch(STEP);

    //public static AtomicInteger count = new AtomicInteger();
    public static void sumForThread(){
         count ++; //之前寫法 非原子操作
        //count.getAndIncrement(); // 原子操作
    }

    public static void main(String[] args)throws Exception {
        Thread[] threads = new Thread[STEP];
        for (int i = 0; i < STEP; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        sumForThread();
                    }
                    countDownLatch.countDown();
                }
            });
            threads[i].start();
        }
        countDownLatch.await();
        System.out.println("*************"+count);
    }

 上面的執行結果count 一直徘徊在 20W左邊,和我們預期就不太一樣,修改之前的 count++;換成了 Atomic原子類操作,這樣每次的執行結果和預期完全相符。

    public static AtomicInteger count = new AtomicInteger();
    public static void sumForThread(){
        // count ++; 之前寫法 非原子操作
        count.getAndIncrement(); // 原子操作
    }

通過查看getAndIncrement()方法源碼得知,是unsafe調用了 getAndAddInt()方法,說到這 說下unsafe類,Java中的Unsafe類爲我們提供了類似C++手動管理內存的能力。Unsafe類,全限定名是sun.misc.Unsafe,從名字中我們可以看出來這個類對普通程序員來說是“危險”的,一般應用開發者不會用到這個類。Unsafe類是"final"的,不允許繼承。且構造函數是private的。

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

繼續查看getAndAddInt()方法源碼,調用的是 compareAndSwapInt()方法,

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 獲取內存中的最新值
            var5 = this.getIntVolatile(var1, var2);
        // 無限循環直到CAS修改成功結束循環
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

4.4 CAS的缺點
CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在幾大問題:

循環時間長開銷很大;

       CAS 通常是配合無限循環一起使用的,我們可以看到 getAndAddInt 方法執行時,如果 CAS 失敗,會一直進行嘗試。如果 CAS 長時間一直不成功,可能會給 CPU 帶來很大的開銷。

只能保證一個變量的原子操作;

當對一個變量執行操作時,我們可以使用循環 CAS 的方式來保證原子操作,但是對多個變量操作時,CAS 目前無法直接保證操作的原子性。但是我們可以通過以下兩種辦法來解決:1)使用互斥鎖來保證原子性;2)將多個變量封裝成對象,通過 AtomicReference 來保證原子性。

ABA問題;

什麼是ABA問題?ABA問題怎麼解決?
       如果內存地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然爲A,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。這個漏洞稱爲CAS操作的“ABA”問題。Java併發包爲了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。
當然上述示例也可以對方法進行synchronized關鍵字修飾 也是一種方案。

如有披露或問題歡迎留言或者入羣探討

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章