你們可能都據說說 Java 中的併發包,若是想要讀懂 Java 中的併發包,其核心就是要先讀懂 CAS 機制,由於 CAS 能夠說是併發包的底層實現原理。java
今天就帶你們讀懂 CAS 是如何保證操做的原子性的,以及 Java8 對 CAS 進行了哪些優化。數組
咱們先來看幾行代碼:安全
public class CASTest { static int i = 0; public static void increment() { i++; } }
假若有100個線程同時調用 increment() 方法對 i 進行自增操做,i 的結果會是 100 嗎?多線程
學會多線程的同窗應該都知道,這個方法是線程不安全的,因爲 i++ 不是一個原子操做,因此是很可貴到 100 的。併發
這裏稍微解釋下爲啥會得不到 100(知道的可直接跳過), i++ 這個操做,計算機須要分紅三步來執行。
一、讀取 i 的值。
二、把 i 加 1.
三、把 最終 i 的結果寫入內存之中。因此,假如線程 A 讀取了 i 的值爲 i = 0,這個時候線程 B 也讀取了 i 的值 i = 0。接着 A把 i 加 1,而後寫入內存,此時 i = 1。緊接着,B也把 i 加 1,此時線程B中的 i = 1,而後線程 B 把 i 寫入內存,此時內存中的 i = 1。也就是說,線程 A, B 都對 i 進行了自增,但最終的結果倒是 1,不是 2.框架
那該怎麼辦呢?解決的策略通常都是給這個方法加個鎖,以下優化
public class CASTest { static int i = 0; public synchronized static void increment() { i++; } }
加了 synchronized 以後,就最多隻能有一個線程可以進入這個 increment() 方法了。這樣,就不會出現線程不安全了。不懂 synchronized 的能夠看我這篇文章:完全搞懂synchronized(從偏向鎖到重量級鎖)操作系統
然而,一個簡簡單單的自增操做,就加了 synchronized 進行同步,好像有點大材小用的感受,加了 synchronized 關鍵詞以後,當有不少線程去競爭 increment 這個方法的時候,拿不到鎖的方法是會被阻塞在方法外面的,最後再來喚醒他們,而阻塞/喚醒這些操做,是很是消耗時間的。.net
這裏可能有人會說,synchronized 到了JDK1.6以後不是作了不少優化嗎?是的,確實作了不少優化,增長了偏向鎖、輕量級鎖等, 可是,就算增長了這些,當不少線程來競爭的時候,開銷依然不少,不信你看我另一篇文章的介紹:完全搞懂synchronized(從偏向鎖到重量級鎖)線程
那有沒有其餘方法來代替 synchronized 對方法的加鎖,而且保證 increment() 方法是線程安全呢?
你們看一下,若是我採用下面這種方式,可否保證 increment 是線程安全的呢?步驟以下:
一、線程從內存中讀取 i 的值,假如此時 i 的值爲 0,咱們把這個值稱爲 k 吧,即此時 k = 0。
二、令 j = k + 1。
三、用 k 的值與內存中i的值相比,若是相等,這意味着沒有其餘線程修改過 i 的值,咱們就把 j(此時爲1) 的值寫入內存;若是不相等(意味着i的值被其餘線程修改過),咱們就不把j的值寫入內存,而是從新跳回步驟 1,繼續這三個操做。
翻譯成代碼的話就是這樣:
public static void increment() { do{ int k = i; int j = k + 1; }while (compareAndSet(i, k, j)) }
若是你去模擬一下,就會發現,這樣寫是線程安全的。
這裏可能有人會說,第三步的 compareAndSet 這個操做不只要讀取內存,還幹了比較、寫入內存等操做,,,這一步自己就是線程不安全的啊?
若是你能想到這個,說明你是真的有去思考、模擬這個過程,不過我想要告訴你的是,這個 compareAndSet 操做,他其實只對應操做系統的一條硬件操做指令,儘管看似有不少操做在裏面,但操做系統可以保證他是原子執行的。
對於一條英文單詞很長的指令,咱們都喜歡用它的簡稱來稱呼他,因此,咱們就把 compareAndSet 稱爲 CAS 吧。
因此,採用 CAS 這種機制的寫法也是線程安全的,經過這種方式,能夠說是不存在鎖的競爭,也不存在阻塞等事情的發生,可讓程序執行的更好。
在 Java 中,也是提供了這種 CAS 的原子類,例如:
具體如何使用呢?我就以上面那個例子進行改版吧,代碼以下:
public class CASTest { static AtomicInteger i = new AtomicInteger(0); public static void increment() { // 自增 1並返回以後的結果 i.incrementAndGet(); } }
雖然這種 CAS 的機制可以保證increment() 方法,但依然有一些問題,例如,當線程A即將要執行第三步的時候,線程 B 把 i 的值加1,以後又立刻把 i 的值減 1,而後,線程 A 執行第三步,這個時候線程 A 是認爲並無人修改過 i 的值,由於 i 的值並無發生改變。而這,就是咱們日常說的ABA問題。
對於基本類型的值來講,這種把數字改變了在改回原來的值是沒有太大影響的,但若是是對於引用類型的話,就會產生很大的影響了。
爲了解決這個 ABA 的問題,咱們能夠引入版本控制,例如,每次有線程修改了引用的值,就會進行版本的更新,雖然兩個線程持有相同的引用,但他們的版本不一樣,這樣,咱們就能夠預防 ABA 問題了。Java 中提供了 AtomicStampedReference 這個類,就能夠進行版本控制了。
因爲採用這種 CAS 機制是沒有對方法進行加鎖的,因此,全部的線程均可以進入 increment() 這個方法,假如進入這個方法的線程太多,就會出現一個問題:每次有線程要執行第三個步驟的時候,i 的值總是被修改了,因此線程又到回到第一步繼續重頭再來。
而這就會致使一個問題:因爲線程太密集了,太多人想要修改 i 的值了,進而大部分人都會修改不成功,白白着在那裏循環消耗資源。
爲了解決這個問題,Java8 引入了一個 cell[] 數組,它的工做機制是這樣的:假若有 5 個線程要對 i 進行自增操做,因爲 5 個線程的話,不是不少,起衝突的概率較小,那就讓他們按照以往正常的那樣,採用 CAS 來自增吧。
可是,若是有 100 個線程要對 i 進行自增操做的話,這個時候,衝突就會大大增長,系統就會把這些線程分配到不一樣的 cell 數組元素去,假如 cell[10] 有 10 個元素吧,且元素的初始化值爲 0,那麼系統就會把 100 個線程分紅 10 組,每一組對 cell 數組其中的一個元素作自增操做,這樣到最後,cell 數組 10 個元素的值都爲 10,系統在把這 10 個元素的值進行彙總,進而獲得 100,二這,就等價於 100 個線程對 i 進行了 100 次自增操做。
固然,我這裏只是舉個例子來講明 Java8 對 CAS 優化的大體原理,具體的你們有興趣能夠去看源碼,或者去搜索對應的文章哦。
理解 CAS 的原理仍是很是重要的,它是 AQS 的基石,而 AQS 又是併發框架的基石,接下來有時間的話,還會寫一篇 AQS 的文章。
最後推廣下個人公衆號:苦逼的碼農:戳我便可關注,文章都會首發於個人公衆號,期待各路英雄的關注交流。