您好,登錄后才能下訂單哦!
一、CAS(Compare And Set)
Compare And Set(或Compare And Swap),CAS是解決多線程并行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數(shù)——內存位置(V)、預期原值(A)、新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。
在java中可以通過鎖和循環(huán)CAS的方式來實現(xiàn)原子操作。Java中 java.util.concurrent.atomic包相關類就是 CAS的實現(xiàn),atomic包里包括以下類:
AtomicBoolean | 可以用原子方式更新的 boolean 值。 |
AtomicInteger | 可以用原子方式更新的 int 值。 |
AtomicIntegerArray | 可以用原子方式更新其元素的 int 數(shù)組。 |
AtomicIntegerFieldUpdater |
基于反射的實用工具,可以對指定類的指定 volatile int 字段進行原子更新。 |
AtomicLong | 可以用原子方式更新的 long 值。 |
AtomicLongArray | 可以用原子方式更新其元素的 long 數(shù)組。 |
AtomicLongFieldUpdater |
基于反射的實用工具,可以對指定類的指定 volatile long 字段進行原子更新。 |
AtomicMarkableReference |
AtomicMarkableReference 維護帶有標記位的對象引用,可以原子方式對其進行更新。 |
AtomicReference |
可以用原子方式更新的對象引用。 |
AtomicReferenceArray |
可以用原子方式更新其元素的對象引用數(shù)組。 |
AtomicReferenceFieldUpdater<T,V> | 基于反射的實用工具,可以對指定類的指定 volatile 字段進行原子更新。 |
AtomicStampedReference |
AtomicStampedReference 維護帶有整數(shù)“標志”的對象引用,可以用原子方式對其進行更新。 |
二、AtomicInteger
AtomicInteger可以用原子方式更新的 int 值。AtomicInteger 可用在應用程序中(如以原子方式增加的計數(shù)器),并且不能用于替換 Integer。但是,此類確實擴展了 Number,允許那些處理基于數(shù)字類的工具和實用工具進行統(tǒng)一訪問。 我們拿 AtomicInteger為例來學習下 CAS操作是如何實現(xiàn)的。
通常情況下,在 Java中,i++等類似操作并不是線程安全的,因為 i++可分為三個獨立的操作:獲取變量當前值,為該值+1,然后寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作時“原子性”的。但是利用加鎖的方式來實現(xiàn)該功能的話,代碼將非常復雜及難以維護,如:
synchronized (lock) { i++; }
相關類中還需要增加 Object lock等額外標志,這樣就帶來了很多麻煩,增加了很多業(yè)務無關代碼,給開發(fā)與維護帶來了不便。
然而利用 atomic包中相關類型就可以很簡單實現(xiàn)此操作,以下是一個計數(shù)程序實例:
public class Counter { private AtomicInteger ai = new AtomicInteger(); private int i = 0; public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> threads = new ArrayList<Thread>(); // 添加100個線程 for (int j = 0; j < 100; j++) { threads.add(new Thread(new Runnable() { public void run() { // 執(zhí)行100次計算,預期結果應該是10000 for (int i = 0; i < 100; i++) { cas.count(); cas.safeCount(); } } })); } //開始執(zhí)行 for (Thread t : threads) { t.start(); } // 等待所有線程執(zhí)行完成 for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("非線程安全計數(shù)結果:"+cas.i); System.out.println("線程安全計數(shù)結果:"+cas.ai.get()); } /** 使用CAS實現(xiàn)線程安全計數(shù)器 */ private void safeCount() { for (;;) { int i = ai.get(); // 如果當前值 == 預期值,則以原子方式將該值設置為給定的更新值 boolean suc = ai.compareAndSet(i, ++i); if (suc) { break; } } } /** 非線程安全計數(shù)器 */ private void count() { i++; } } /** 非線程安全計數(shù)結果:9942 線程安全計數(shù)結果:10000 */
其中非線程安全計數(shù)器所計算的結果每次都不相同且不正確,而線程安全計數(shù)器計算的結果每次都是正確的。
三、存在的問題
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、循環(huán)時間長開銷大、只能保證一個共享變量的原子操作。
ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的 atomic包里提供了一個類AtomicStampedReference 來解決ABA問題。這個類的 compareAndSet方法作用是首先檢查當前引用是否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。
循環(huán)時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環(huán)的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執(zhí)行效率?!?/p>
只能保證一個共享變量的原子操作:當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。