原子型別的動機

實現多執行緒應用程式的簡單方法是使用 Java 的內建同步和鎖定原語; 例如 synchronized 關鍵字。以下示例顯示了我們如何使用 synchronized 來累積計數。

public class Counters {
    private final int[] counters;

    public Counters(int nosCounters) {
        counters = new int[nosCounters];
    }

    /**
     * Increments the integer at the given index
     */
    public synchronized void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number]++;
        }
    }

    /**
     * Obtains the current count of the number at the given index,
     * or if there is no number at that index, returns 0.
     */
    public synchronized int getCount(int number) {
        return (number >= 0 && number < counters.length) ? counters[number] : 0;
    }
}

此實現將正常工作。但是,如果你有大量執行緒在同一 Counters 物件上進行大量同時呼叫,則同步可能會成為瓶頸。特別:

  1. 每個 synchronized 方法呼叫將從當前執行緒獲取 Counters 例項的鎖定開始。
  2. 執行緒將在檢查 number 值時保持鎖定並更新計數器。
  3. 最後,它將釋放鎖,允許其他執行緒訪問。

如果一個執行緒嘗試獲取鎖定而另一個執行緒持有該鎖定,則在步驟 1 將阻止(停止)嘗試執行緒,直到鎖定被釋放。如果多個執行緒正在等待,其中一個執行緒將獲取它,其他執行緒將繼續被阻止。

這可能會導致一些問題:

  • 如果對鎖有很多爭用 (即許多執行緒試圖獲取它),那麼一些執行緒可能會被阻塞很長時間。

  • 當執行緒被阻塞等待鎖定時,作業系統通常會嘗試將執行切換到不同的執行緒。該上下文切換對處理器產生相對大的效能影響。

  • 當同一個鎖上有多個執行緒被阻塞時,不能保證它們中的任何一個都會被公平地處理(即保證每個執行緒都被安排執行)。這可能導致執行緒飢餓

如何實現原子型別?

讓我們從使用 AtomicInteger 計數器重寫上面的例子開始:

public class Counters {
    private final AtomicInteger[] counters;

    public Counters(int nosCounters) {
        counters = new AtomicInteger[nosCounters];
        for (int i = 0; i < nosCounters; i++) {
            counters[i] = new AtomicInteger();
        }
    }

    /**
     * Increments the integer at the given index
     */
    public void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number].incrementAndGet();
        }
    }

    /**
     * Obtains the current count of the object at the given index,
     * or if there is no number at that index, returns 0.
     */
    public int getCount(int number) {
        return (number >= 0 && number < counters.length) ? 
                counters[number].get() : 0;
    }
}

我們用 AtomicInteger[] 替換了 int[],並在每個元素中用一個例項初始化它。我們還新增了對 incrementAndGet()get() 的呼叫來代替 int 值的操作。

但最重要的是我們可以刪除 synchronized 關鍵字,因為不再需要鎖定。這是有效的,因為 incrementAndGet()get() 操作是原子的執行緒安全的。在這種情況下,它意味著:

  • 陣列中的每個計數器只能在操作的之前狀態(如增量)或之後狀態下觀察到

  • 假設操作發生在時間 T,沒有執行緒在時間 T 之後將能夠看到之前狀態。

此外,雖然兩個執行緒實際上可能同時嘗試更新同一個 AtomicInteger 例項,但操作的實現確保在給定例項上一次只發生一個增量。這是在沒有鎖定的情況下完成的,通常會帶來更好的性

原子型別如何工作?

原子型別通常依賴於目標機器的指令集中的專用硬體指令。例如,基於 Intel 的指令集提供了一個 CAS比較和交換 )指令,它將以原子方式執行特定的儲存器操作序列。

這些低階指令用於在相應的 AtomicXxx 類的 API 中實現更高階別的操作。例如,(再次,在類似 C 的虛擬碼中):

private volatile num;

int increment() {
  while (TRUE) {
    int old = num;
    int new = old + 1;
    if (old == compare_and_swap(&num, old, new)) {
      return new;
    }
  }
}

如果 AtomicXxxx 沒有爭用,則 if 測試將成功,迴圈將立即結束。如果存在爭用,則 if 將對除了一個執行緒之外的所有執行緒都失敗,並且它們將在迴圈中旋轉以進行少量迴圈迴圈。在實踐中,旋轉速度提高了幾個數量級(除了在不切實際的高級別爭用中,同步效能優於原子類,因為當 CAS 操作失敗時,重試只會增加爭用),而不是暫停執行緒並切換到另一個一。

順便提一下,JVM 通常使用 CAS 指令來實現無競爭鎖定。如果 JVM 可以看到鎖當前未被鎖定,它將嘗試使用 CAS 來獲取鎖。如果 CAS 成功,則無需進行昂貴的執行緒排程,上下文切換等。有關所用技術的更多資訊,請參閱 HotSpot 中的偏置鎖定