【多執行緒系列】CAS、AQS簡單介紹

zybing發表於2021-09-09

什麼是CAS

CAS(Compare And Swap),即比較並交換。是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,CAS操作包含三個運算元——記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。
在JAVA中,sun.misc.Unsafe 類提供了硬體級別的原子操作來實現這個CAS。 java.util.concurrent 包下的大量類都使用了這個 Unsafe.java 類的CAS操作。

CAS典型應用

java.util.concurrent.atomic 包下的類大多是使用CAS操作來實現的,比如 AtomicInteger、AtomicBoolean、AtomicLong。一般來說在競爭不是特別激烈的時候,使用該包下的原子操作效能比使用 synchronized 關鍵字的方式高效的多(檢視getAndSet(),可知如果資源競爭十分激烈的話,這個for迴圈可能會持續很久都不能成功跳出。不過這種情況可能需要考慮降低資源競爭才是)。
在較多的場景我們都可能會使用到這些原子類操作。一個典型應用就是計數了,在多執行緒的情況下需要考慮執行緒安全問題。
一、現在有這麼一個需求,需要實現一個支援併發的計數功能,例如下面的程式碼
圖片描述
在併發環境下對 count 進行自增運算是不安全的,為什麼不安全以及如何解決這個問題呢?

二、為什麼併發環境下的count自增操作不安全

因為count++不是原子操作,而是三個原子操作的組合:

  1. 讀取記憶體中的count值賦值給區域性變數temp
  2. 執行temp+1操作
  3. 將temp賦值給count

所以如果兩個執行緒同時執行 count++ 操作的話,我們不能保證執行緒一按順序執行完上述三步後執行緒二才開始執行。

三、併發環境下count++不安全問題的解決方案
方案一:synchronized 加鎖
圖片描述
加鎖後程式碼如上,同一時間只有一個執行緒能加鎖,其他執行緒需要等待鎖,這樣就不會出現count 計數不準確的問題了,現線上程安全。。

但是引入synchronized會造成多個執行緒排隊的問題,同一時間只有一個執行緒執行,這樣的鎖有點兒“重量級”了。這類似於悲觀鎖的實現,我需要獲取這個資源,那麼我就給它加鎖,別的執行緒都無法訪問該資源,直到我操作完後釋放對該資源的鎖。雖然隨著Java版本更新,也對synchronized做了很多最佳化,但是處理這種簡單的累加操作,仍然顯得“太重了”。人家synchronized是可以解決更加複雜的併發程式設計場景和問題的。

而且在這個場景下,若用synchronized,不就相當於讓各個執行緒序列化了麼?一個接一個的排隊、加鎖、處理資料、釋放鎖,下一個再進來。

方案二:Atomic 原子類

對於這種的count++類的操作,我們完全可以換一種做法,java併發包下面提供了一系列的Atomic原子類,比如說AtomicInteger
圖片描述
多個執行緒可以併發的執行 AtomicInteger 的 incrementAndGet() 方法,意思就是給我把count的值累加1,接著返回累加後最新的值。實際上,Atomic原子類底層用的不是傳統意義的鎖機制,而是無鎖化的CAS機制,透過CAS機制保證多執行緒修改一個數值的安全性。

四、CAS底層實現原理是什麼
流程圖如下:
圖片描述
假如說有3個執行緒併發的要修改一個AtomicInteger的值,他們底層的機制如下:

  1. 首先,每個執行緒都會先獲取當前的值。接著走一個原子的CAS操作,原子的意思就是這個CAS操作一定是自己完整執行完的,不會被別人打斷。
  2. 然後CAS操作裡,會比較一下,現在你的值是不是剛才我獲取到的那個值。如果是,說明沒人改過這個值,那你給我設定成累加1之後的一個值。
  3. 同理,如果有人在執行CAS的時候,發現自己之前獲取的值跟當前的值不一樣,會導致CAS失敗,失敗之後,進入一個無限迴圈,再次獲取值,接著執行CAS操作。

CAS有沒有什麼問題?

五、CAS效能最佳化
從上面的流程圖其實可以看出來,比如說大量的執行緒同時併發修改一個 AtomicInteger,可能有很多執行緒會不停的自旋,進入一個無限重複的迴圈中。
這些執行緒不停地獲取值,然後發起CAS操作,但是發現這個值被別人改過了,於是再次進入下一個迴圈,獲取值,發起CAS操作又失敗了,再次進入下一個迴圈。
在大量執行緒高併發更新 AtomicInteger 的時候,這種問題可能會比較明顯,導致大量執行緒空迴圈,自旋轉,效能和效率都不是特別好。那麼如何最佳化呢?

Java 8有一個新的類,LongAdder,他就是嘗試使用分段CAS以及自動分段遷移的方式來大幅度提升多執行緒高併發執行CAS操作的效能,這個類具體是如何最佳化效能的呢?如圖:
圖片描述
LongAdder核心思想就是熱點分離,這一點和ConcurrentHashMap的設計思想相似。就是將value值分離成一個陣列,當多執行緒訪問時,透過hash演算法對映到其中的一個數字進行計數。而最終的結果,就是這些陣列的求和累加。這樣一來,就減小了鎖的粒度。

LongAddr的兄弟類如下:
圖片描述

什麼是AQS

AQS(AbstractQueuedSynchronizer),AQS是JDK下提供的一套用於實現基於FIFO等待佇列的阻塞鎖和相關的同步器的一個同步框架。它使用了一個原子的int value status來作為同步器的狀態(如:獨佔鎖,1代表已佔有,0代表未佔有),透過該類提供的原子修改status方法(getState setState and compareAnsSetState),我們可以把它作為同步器的基礎框架類來實現各種同步器。AQS還定義了一個實現了Condition介面的ConditionObject內部類。Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的物件,以便透過將這些物件與任意 Lock 實現組合使用,為每個物件提供多個等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。
簡單來說,就是Condition提供類似於Object的wait、notify的功能signal和await,都是可以使一個正在執行的執行緒掛起(推遲執行),直到被其他執行緒喚醒。但是Condition更加強大,如支援多個條件謂詞、保證執行緒喚醒的順序和在掛起時不需要擁有鎖。這個抽象類被設計為作為一些可用原子int值來表示狀態的同步器的基類。如果你有看過類似 CountDownLatch 類的原始碼實現,會發現其內部有一個繼承了 AbstractQueuedSynchronizer 的內部類 Sync。可見 CountDownLatch 是基於AQS框架來實現的一個同步器.類似的同步器在JUC下還有不少。(eg. Semaphore)

AQS用法

如上所述,AQS管理一個關於狀態資訊的單一整數,該整數可以表現任何狀態。比如, Semaphore 用它來表現剩餘的許可數,ReentrantLock 用它來表現擁有它的執行緒已經請求了多少次鎖;FutureTask 用它來表現任務的狀態(尚未開始、執行、完成和取消)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2822451/,如需轉載,請註明出處,否則將追究法律責任。

相關文章