Java原子類通過cas機制實現多線程下變量操作的原子性。1.其核心原理是利用cpu原子指令結合volatile關鍵字,確保變量可見性和原子操作;2.cas包含內存位置、預期值和新值三個操作數,若匹配成功則更新,否則重試;3.aba問題可通過atomicstampedreference添加版本號解決;4.性能瓶頸在于自旋重試消耗cpu資源,優化方式包括減少競爭、使用longadder分段累加、選擇合適原子類及避免長時間自旋;5.除cas外,鎖機制如synchronized或reentrantlock也可實現原子操作,但帶來線程阻塞開銷,需根據并發場景權衡選用。
Java原子類,簡單來說,就是提供原子操作的類,保證多線程環境下對變量進行操作的原子性。實現原理核心在于CAS(Compare and Swap)機制,一種無鎖并發編程的基石。
解決方案
Java的java.util.concurrent.atomic包下提供了多種原子類,例如AtomicInteger、AtomicLong、AtomicBoolean等。它們利用了CPU提供的原子指令,結合volatile關鍵字,實現了對變量的原子操作。
立即學習“Java免費學習筆記(深入)”;
CAS機制包含三個操作數:需要讀寫的內存位置(V)、預期原值(A)和新值(B)。CAS操作嘗試將內存位置V的值原子性地更新為新值B,前提是V的值必須與預期原值A相匹配。如果匹配成功,處理器會自動完成更新。如果不匹配,說明其他線程已經修改了V的值,則當前線程會放棄更新,通常會選擇重試。
以AtomicInteger為例,其incrementAndGet()方法就是一個典型的CAS操作。其內部實現大致如下:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } public final int get() { return value; } public final boolean compareAndSet(int expectedValue, int newValue) { // Native method using CPU atomic instructions // 偽代碼,實際實現依賴于底層CPU指令 if (value == expectedValue) { value = newValue; return true; } else { return false; } }
這個循環會不斷嘗試更新value,直到CAS操作成功。如果value在當前線程嘗試更新時被其他線程修改,compareAndSet會返回false,導致循環繼續,直到成功為止。
CAS機制的ABA問題如何解決?
ABA問題是指在CAS操作中,變量的值先從A變為B,然后再變回A。雖然CAS操作檢查到值仍然是A,認為沒有被修改,但實際上可能已經被修改過了。
解決ABA問題的一種常見方法是使用版本號(Version number)或時間戳(timestamp)。每次變量被修改時,版本號或時間戳都會遞增。在CAS操作時,不僅要比較變量的值,還要比較版本號或時間戳。如果版本號或時間戳不一致,說明變量已經被修改過,即使值仍然是A,也應該放棄更新。
Java中可以使用AtomicStampedReference類來解決ABA問題。AtomicStampedReference維護了變量的值和一個整數型的版本號,可以原子性地更新值和版本號。
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0); // 線程1 int stamp = atomicStampedRef.getStamp(); Integer reference = atomicStampedRef.getReference(); // 假設reference被修改為其他值,然后又變回100 boolean success = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1); if (success) { System.out.println("線程1修改成功"); } else { System.out.println("線程1修改失敗"); } // 線程2 int stamp2 = atomicStampedRef.getStamp(); Integer reference2 = atomicStampedRef.getReference(); boolean success2 = atomicStampedRef.compareAndSet(100, 101, stamp2, stamp2 + 1); if (success2) { System.out.println("線程2修改成功"); } else { System.out.println("線程2修改失敗"); }
即使reference的值仍然是100,由于stamp已經被修改,compareAndSet操作也會失敗,從而避免了ABA問題。
原子類的性能瓶頸是什么?如何優化?
原子類的性能瓶頸主要在于CAS操作的自旋重試。在高并發場景下,如果多個線程同時競爭同一個原子變量,可能會導致大量的自旋重試,消耗CPU資源。
優化原子類的性能可以考慮以下幾個方面:
-
減少競爭: 盡量避免多個線程同時競爭同一個原子變量。可以通過ThreadLocal、分段鎖等方式將競爭分散到不同的變量上。
-
使用LongAdder: 對于累加操作,可以使用LongAdder類。LongAdder內部維護了多個Cell,每個Cell相當于一個獨立的累加器。多個線程可以同時在不同的Cell上進行累加,最后再將所有Cell的值加起來。這樣可以減少線程之間的競爭。
-
減少內存訪問: 原子操作需要頻繁地訪問內存,而內存訪問是比較耗時的操作。可以通過將原子變量緩存在CPU的Cache中來減少內存訪問。但是需要注意Cache一致性問題。
-
選擇合適的原子類: 不同的原子類適用于不同的場景。例如,AtomicInteger適用于簡單的整數原子操作,而AtomicReference適用于對象引用的原子操作。選擇合適的原子類可以提高性能。
-
避免長時間的自旋: 如果自旋時間過長,可能會導致CPU資源浪費。可以考慮在自旋一定次數后,讓線程休眠一段時間,或者放棄更新。
除了CAS,還有其他實現原子操作的方式嗎?
除了CAS,還可以使用鎖機制來實現原子操作。例如,可以使用synchronized關鍵字或ReentrantLock類來對臨界區進行加鎖,保證只有一個線程可以訪問臨界區。
private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; } }
或者使用ReentrantLock:
private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } }
鎖機制可以保證原子性,但是會帶來線程阻塞的開銷。CAS機制是一種無鎖的并發編程方式,可以避免線程阻塞的開銷,但是可能會導致自旋重試。
選擇使用CAS還是鎖機制,需要根據具體的場景進行權衡。在高并發、低競爭的場景下,CAS機制通常比鎖機制更高效。在低并發、高競爭的場景下,鎖機制可能更適合。