Java原子類通過cas實現多線程安全變量修改,避免鎖機制。1.核心原理是利用cas指令比較并交換內存值,確保操作原子性;2.常見類如atomicinteger、atomiclong等適用于計數器、標志位等簡單更新場景;3.cas優勢在于減少上下文切換、提高并發性及更細粒度控制;4.在高競爭或復雜邏輯時仍需使用鎖;5.aba問題可通過atomicstampedreference引入版本號解決;6.不同原子類適用場景各異,如longadder用于高并發計數,atomicreference處理引用更新。
Java原子類本質上提供了一種在多線程環境下安全修改單個變量的方式,避免了顯式的鎖機制。它們利用了底層的CAS(Compare and Swap)指令,實現了無鎖并發,在特定場景下可以顯著提升性能。
解決方案
Java的java.util.concurrent.atomic包提供了一系列原子類,比如AtomicInteger、AtomicLong、AtomicBoolean等。這些類內部封裝了一個變量,并提供了原子性的get()、set()、incrementAndGet()等方法。
立即學習“Java免費學習筆記(深入)”;
CAS操作包含三個操作數:內存位置(V)、預期原值(A)和新值(B)。CAS指令執行時,首先比較內存位置V的值是否等于預期原值A,如果相等,那么處理器會自動將該位置的值更新為新值B。如果不相等,說明在此期間有其他線程修改了該變量,CAS操作失敗,通常需要進行重試。
例如,AtomicInteger的incrementAndGet()方法可以這樣理解:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
這段代碼使用一個無限循環,不斷嘗試將當前值加1。compareAndSet(current, next)方法就是CAS操作,它會比較當前值是否等于current,如果相等,則更新為next并返回true,否則返回false。如果CAS操作失敗,循環會繼續,直到成功為止。
CAS的優勢:
- 避免了上下文切換: 鎖機制在競爭激烈時會導致線程阻塞,阻塞的線程需要進行上下文切換,這會帶來額外的開銷。CAS操作不會阻塞線程,失敗的線程會重試,避免了上下文切換的開銷。
- 更高的并發性: 無鎖算法通常具有更高的并發性,因為它們允許多個線程同時嘗試修改變量,只要只有一個線程能夠成功即可。
- 更細粒度的控制: 原子類允許對單個變量進行原子性操作,而鎖機制通常需要保護整個代碼塊,原子類提供了更細粒度的控制,可以減少鎖的競爭范圍。
原子類比鎖性能更好嗎?什么時候應該使用原子類?
原子類并非在所有情況下都優于鎖。CAS操作雖然避免了阻塞,但如果競爭非常激烈,線程會不斷重試,消耗CPU資源,導致性能下降。
何時使用原子類:
- 低競爭環境: 當并發量不高,線程之間競爭不激烈時,原子類可以發揮其優勢,避免鎖的開銷。
- 簡單的數據更新: 原子類適用于簡單的變量更新操作,比如計數器、標志位等。
- 性能敏感的應用: 在性能至關重要的應用中,可以考慮使用原子類來減少鎖的開銷。
何時使用鎖:
- 高競爭環境: 當并發量很高,線程之間競爭激烈時,鎖機制可能更有效,因為它可以避免線程不斷重試,減少CPU消耗。
- 復雜的數據更新: 當需要對多個變量進行原子性操作,或者需要執行復雜的邏輯時,鎖機制更適合。
- 需要保證公平性: CAS操作不保證公平性,可能導致某些線程一直無法成功。如果需要保證公平性,可以使用公平鎖。
選擇原子類還是鎖,需要根據具體的應用場景進行權衡。通常,可以先使用原子類進行嘗試,如果性能不佳,再考慮使用鎖。
ABA問題是什么?如何解決?
ABA問題是CAS操作中一個潛在的問題。假設一個線程讀取了變量的值A,在它準備執行CAS操作時,另一個線程將變量的值從A改成了B,又從B改回了A。這樣,當第一個線程執行CAS操作時,會發現變量的值仍然是A,CAS操作會成功,但實際上變量已經被修改過了。
舉例:
- 線程1讀取變量AtomicInteger的值為10。
- 線程2將AtomicInteger的值改為20。
- 線程3又將AtomicInteger的值改回10。
- 線程1執行CAS操作,發現值仍然是10,于是成功將值改為11。
盡管線程1的CAS操作成功了,但實際上AtomicInteger的值已經被修改過了,這可能會導致一些意想不到的問題。
解決方案:
ABA問題的解決方案是引入版本號或者時間戳。每次變量被修改時,版本號或者時間戳都會增加。這樣,即使變量的值相同,版本號或者時間戳也不同,CAS操作會失敗。
Java中可以使用AtomicStampedReference和AtomicMarkableReference來解決ABA問題。
- AtomicStampedReference:維護一個對象引用以及一個整數值“stamp”,可以原子性地更新二者。stamp可以看作是版本號。
- AtomicMarkableReference:維護一個對象引用以及一個boolean值“mark”,可以原子性地更新二者。mark可以用來標記對象是否被修改過。
使用AtomicStampedReference的示例:
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(10, 0); int stamp = atomicRef.getStamp(); Integer value = atomicRef.getReference(); // 模擬ABA問題 new Thread(() -> { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } atomicRef.compareAndSet(10, 20, stamp, stamp + 1); atomicRef.compareAndSet(20, 10, stamp + 1, stamp + 2); }).start(); new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } int currentStamp = atomicRef.getStamp(); boolean success = atomicRef.compareAndSet(10, 11, currentStamp, currentStamp + 1); System.out.println("Thread3 CAS result: " + success); // 輸出:Thread3 CAS result: false }).start();
在這個例子中,線程1嘗試將AtomicStampedReference的值從10改為11,但由于線程2修改了值,導致版本號發生了變化,CAS操作失敗。
除了AtomicInteger,Java還提供了哪些原子類?它們分別適用于什么場景?
Java的java.util.concurrent.atomic包提供了多種原子類,以滿足不同的需求:
- AtomicInteger: 原子整型,適用于計數器、序列號生成等場景。
- AtomicLong: 原子長整型,適用于需要更大范圍的計數器、統計等場景。
- AtomicBoolean: 原子布爾型,適用于標志位、開關等場景。
- AtomicReference
: 原子引用,適用于需要原子性地更新對象引用的場景。 - AtomicStampedReference
: 原子標記引用,解決了ABA問題,適用于需要保證對象引用在更新過程中沒有被修改過的場景。 - AtomicMarkableReference
: 原子可標記引用,與AtomicStampedReference類似,但使用boolean值來標記對象是否被修改過。 - AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
: 原子數組,分別用于原子性地更新整型數組、長整型數組和對象數組中的元素。 - LongAdder、DoubleAdder: 高并發計數器,在高并發環境下比AtomicLong和AtomicDouble具有更好的性能。
- LongAccumulator、DoubleAccumulator: 通用累加器,可以自定義累加函數,適用于更復雜的累加場景。
選擇合適的原子類需要根據具體的應用場景進行考慮。如果只需要簡單的計數,AtomicInteger或AtomicLong就足夠了。如果需要解決ABA問題,可以使用AtomicStampedReference或AtomicMarkableReference。如果需要高并發計數,可以使用LongAdder或DoubleAdder。如果需要自定義累加邏輯,可以使用LongAccumulator或DoubleAccumulator。