原子型別的動機
實現多執行緒應用程式的簡單方法是使用 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
物件上進行大量同時呼叫,則同步可能會成為瓶頸。特別:
- 每個
synchronized
方法呼叫將從當前執行緒獲取Counters
例項的鎖定開始。 - 執行緒將在檢查
number
值時保持鎖定並更新計數器。 - 最後,它將釋放鎖,允許其他執行緒訪問。
如果一個執行緒嘗試獲取鎖定而另一個執行緒持有該鎖定,則在步驟 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 中的偏置鎖定 。