原子操作出現的先決條件
讓我們看一下這個示例,以幫助您了解原子操作的工作原理:
public class Counter {
int count;
public void increment() {
count++;
}
}
當我們有一個線程時,一切都很好,但是如果我們添加多線程,我們會得到錯誤的結果,這都是因為增量操作不是一個操作,而是三個:獲取當前值的請求數數,然後將其遞增 1 並再次寫入數數.
當兩個線程想要增加一個變量時,您很可能會丟失數據。也就是說,兩個線程都收到 100,因此,兩者都將寫入 101 而不是預期值 102。
以及如何解決?你需要使用鎖。synchronized關鍵字有助於解決這個問題,使用它可以保證一個線程一次訪問該方法。
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
另外,您需要添加volatile關鍵字,以確保線程間引用的正確可見性。我們在上面回顧了他的工作。
但仍有缺點。最大的一個是性能,在那個時間點,當許多線程試圖獲取鎖並且一個獲得寫機會時,其餘線程將被阻塞或掛起,直到線程被釋放。
所有這些進程、阻塞、切換到另一個狀態對於系統性能來說都是非常昂貴的。
原子操作
該算法使用低級機器指令,例如比較和交換(CAS,compare-and-swap,確保數據完整性並且已經有大量研究)。
典型的 CAS 操作對三個操作數進行操作:
- 工作內存空間(M)
- 變量的現有預期值 (A)
- 要設置的新值 (B)
CAS 以原子方式將 M 更新為 B,但前提是 M 的值與 A 相同,否則不採取任何操作。
在第一種和第二種情況下,都會返回M的值,這樣可以將獲取值、比較值、更新值三個步驟結合起來。這一切都變成了機器級別的一次操作。
當多線程應用程序訪問變量並嘗試更新它並應用 CAS 時,其中一個線程將獲取它並能夠更新它。但與鎖不同的是,其他線程只會收到無法更新值的錯誤。然後他們將繼續進行進一步的工作,而這種工作完全排除了轉換。
在這種情況下,邏輯變得更加困難,因為我們必須處理 CAS 操作未成功運行的情況。我們將只對代碼建模,以便在操作成功之前它不會繼續。
原子類型簡介
您是否遇到過需要為最簡單的int類型變量設置同步的情況?
我們已經介紹過的第一種方法是使用volatile + synchronized。但也有特殊的 Atomic* 類。
如果我們使用 CAS,那麼與第一種方法相比,操作工作得更快。此外,我們還有特殊且非常方便的方法來添加值以及遞增和遞減操作。
AtomicBoolean、 AtomicInteger、 AtomicLong、 AtomicIntegerArray、 AtomicLongArray是其中操作是原子的類。下面我們將與他們一起分析工作。
原子整數
AtomicInteger類除了提供擴展的原子操作外,還提供對可以原子方式讀取和寫入的int值的操作。
它具有get和set方法,其工作方式類似於讀取和寫入變量。
也就是說,“先於發生”與我們之前討論過的相同變量的任何後續接收。原子compareAndSet方法也具有這些內存一致性功能。
所有返回新值的操作都是原子執行的:
int addAndGet (int 增量) | 將特定值添加到當前值。 |
布爾比較和設置(預期 int,更新 int) | 如果當前值與預期值匹配,則將值設置為給定的更新值。 |
int decrementAndGet() | 將當前值減一。 |
int getAndAdd(int 增量) | 將給定值添加到當前值。 |
int getAndDecrement() | 將當前值減一。 |
int getAndIncrement() | 將當前值增加一。 |
int getAndSet(int newValue) | 設置給定值並返回舊值。 |
int incrementAndGet() | 將當前值增加一。 |
惰性集(int newValue) | 最後設置為給定值。 |
布爾 weakCompareAndSet(預期,更新 int) | 如果當前值與預期值匹配,則將值設置為給定的更新值。 |
例子:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION