从原子性挖到CAS
之前在分析volatile關鍵字的時候有提到了volatile不能保證原子性,而且書上提到可以通過加鎖(synchronized和JUC中的原子類)來保證原子性。這邊炒一波冷飯,對于synchronized我也在上篇討論過它在同一時間只能允許一個線程去訪問一段特定的代碼,從而保護一些變量或者數據不會被其他線程所修改來實現原子性。至于為什么,上篇中也有解釋這里就不再贅述了。對于Java JUC包中的原子類,它們的底層就是通過CAS來實現對變量的原子操作。在平常看書或者看其他大神博客的過程中,CAS這個概念經常會被提起,既然不明白為什么不進行了解一番呢?
CAS(compare and swap)是什么?
(宏觀來說)在計算機科學中,比較和交換(CAS)是多線程中用于實現同步的原子指令(來自Wikipedia)。(回到現實來說)CAS中包含三個參數,內存中的值V、期望值A和要修改的值B。 拿期望值A與內存中的值V進行比較,如果相同,則用B替換內存中的值V,否則什么都不做。如果單線程的情況下,所有的操作都是一個線程來完成,就不需要考慮其他的問題。但是如果是多線程的話,同時訪問內存就會有不一樣的情況。
如果A、B線程能自覺的排隊去訪問主內存中的值就沒這篇文章什么事了。 在某一時刻,線程A、B同時想去修改主內存中的共享變量。根據CAS原則有3個步驟要走,讀值比較替換。它們同時讀到了內存中的值V,并將值以期望值A的形式存到自己的工作內存(根據Java內存模型,每個線程有自己的工作內存來對變量進行操作)中。接著就是線程進行寫值操作的時候了,內存中的值只有一個,若想操作它總要有個先來后到。若A線程眼疾手快搶占先機(假設這時并沒有第三個線程改變內存中的值V),用期望值A與內存中的值V比較,發現一致然后將自己的更新值B替換主內存的值V。然后線程B來了,在做比較的時候發現自己的期望值A與內存中的值V不相等,只能悻悻而歸。
CAS的優點?
上面說到,volatile關鍵字不能保證原子性,開發過程中可以通過加鎖的形式來保證。這里要鞭尸一波synchronized關鍵字,多線程的情況下,它能保證原子性。但是當一個線程獲得鎖后,其他線程就被掛起,當獲得鎖的線程釋放鎖后其他線程才能重新去競爭鎖。每一次線程的阻塞和喚醒都需要操作系統的介入,需要在用戶態和和內核態之間切換,而這種切換會消耗大量的系統資源。而使用CAS基于指令來實現,不需要進入內核或者切換線程,所以性能上會比synchronized關鍵字要好。
CAS的缺陷?
上面講了它的有點,現在嘴臭一波。
在并發量大的情況下,CAS自旋的概率會變大。若多個線程反復的去嘗試更新一個變量卻一直不成功,會一直循環等待重試,直到耗盡CPU分配給該線程的時間片,對CPU造成巨大的壓力。
CAS機制只能保證一個變量的原子性操作,多個變量時還是只能使用synchronized關鍵字。
在線程讀取變量和替換變量值的過程中存在一定的時間差,在這個時間差中內存中的變量值可能從A變成B再變成A,當前線程無法判斷當前V值是否發生變化。對于如何解決這個ABA的問題,《并發編程藝術》中給出為每一個變量添加標識,一旦對變量的值修改后,對標識也進行操作。在每次CAS比較的過程中,同時去比較標識的值來判斷當前的V值是否發生變化。Java提供了AtomicStampedReference來解決ABA的問題,它通過創建Pair內部對象來維護標記的引用。源碼部分也還好理解的, 當前的引用和標識與預期的引用和標識相等,并且更新后的引用和標志與當前的引用和標志相等則直接返回true,否則通過生成一個新的Pair對象與當前Pair進行CAS替換。
CAS具體的使用場景?
這里也就拿上面說到的JUC下原子操作類來舉例子。原子操作類是在java.util.concurrent.atomic包下一系列以Atomic開頭的包裝類。AtomicInteger也可以保證共享變量在多線程環境下是線程安全的。
AtomicInteger源碼
public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe = Unsafe.getUnsafe();// value成員屬性的內存地址相對于對象內存地址的偏移量private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;public final int get() {return value;}public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);} } 復制代碼在調試AtomicInteger源碼時發現,不同的AtomicInteger的對象中valueOffset的值都是相同的,由此產生疑問,最后查閱資料發現valueOffSet為value成員屬性的內存地址相對于對象內存地址的偏移量。
Unsafe是CAS的核心類,Java方法無法直接訪問底層的系統,需要通過本地方法(native)來訪問。Unsafe可以直接操作特定的內存數據。value值用volatile關鍵字修飾,保證了變量在多線程操作中的內存可見性。
馬后炮
對于線程、鎖這邊的知識點很多都是互相關聯的,很難對這邊這么多的概念進行一個系統的羅列。最近寫的幾篇都是偏理論的而且大部分還是書上的內容,寫的還是比較虛,不過每次在推敲著寫的時候會發現很多不起眼的其他相關知識,如果感覺有用還是會貼到文章的句子中,還是希望繼續寫下去的時候能多有一些自己的想法。
最后還是那句話,學習的最終目的并不是為了面試,面試只是一個激勵學習的動機。把握面試題,享受學習新知識的樂趣。
參考:
《并發編程的藝術》
總結
- 上一篇: nginx+keepalived 高可用
- 下一篇: Python全栈(第一部分)day2