傻瓜式的解答:為什麼原子運算 AtomicInteger 可以解決多執行緒計算臨界資源錯誤
阿新 • • 發佈:2018-12-21
/* 為什麼多執行緒計算臨界資源會錯誤? 原貼:https://www.cnblogs.com/wxd0108/p/5479442.html 答:多執行緒的記憶體模型分【主存】和【執行緒棧】,在處理資料時,執行緒會把值從主存load到本地棧,完成操作後再save回去 例如:2個執行緒執行 i++ 1、執行緒1 load i的值(0) 到本地棧 2、執行緒2 load i的值(0) 到本地棧 3、執行緒1 save i的值(1) 到主存 4、執行緒2 save i的值(1) 到主存 5、最終,主存 i = 1 若是原子操作呢: 網上對於原子操作原理的解釋是:https://www.jianshu.com/p/9ff426a784ad 用CPU的一條指令完成這麼一個操作:比較 A“要更新的變數” 與 B“變數的預期值” 若兩者相同,將 A 設為 C“變數的新值” 使用CPU的一條指令,所以是原子行為可以理解。但是A、B、C如何帶入上面多執行緒的公式呢? i.compareAndSet(0,1) //這一句就是具體的使用, 比較 i == 0 則返回 true 並讓 i = 1; 由於底層是CPU的一條指令,這行程式碼是原子性的,是不會同時執行的,但顯然單單這行程式碼沒什麼卵用 //看看官方的使用例子 fun incrementAndGet(){//官方的原子自加操作 for(;;){ int current = get(); //儲存當前的值 int next = current + 1; //計算出++後的值 if(compareAndSet(current,next)){ //若此時當前的值與自身值相同,則原子的自加一 return next; //死迴圈直到計算成功 } } } 看到這例子時,我有個疑問: 這一堆是什麼鬼?直接使用 i.compareAndSet(i,i+1)來實現原子自加不行嗎? 線上程操作時要把程式碼分解成不能再分解的地步: ———————————————————————————————————————————————————— 實驗1: val next = i + 1 //設 i = 0 i.compareAndSet(i,next) //原子操作,無法分解 假如是這麼執行的: 1、執行緒1 計算出 next = 1 2、執行緒2 計算出 next = 1 3、執行緒1 執行 compareAndSet 導致 i = next = 1 4、執行緒2 執行 compareAndSet 導致 i = next = 1 5、最終運算得 i = 1 而不是 i = 2 在上述操作中,參B“變數的預期值”是不可靠的,它由於執行緒的計算變化了,這種不可靠導致行2 錯誤的執行成功了。 那麼讓 參B 變得可靠就行了吧。 ———————————————————————————————————————————————————— 實驗2: val current = i //設 i = 0 val next = current + 1 //不能使用 i + 1, i 是不可靠的變數,而 current 是常量 i.compareAndSet(current,next) 執行假設: 1、執行緒1 計算出 current = 0 2、執行緒2 計算出 current = 0 3、執行緒1 計算出 next = 1 4、執行緒2 計算出 next = 1 5、執行緒1 執行 compareAndSet:i(0) == current(0),i = 1 6、執行緒2 執行 compareAndSet:i(1) != current(0) 7、最終運算得 i = 1 第二次實驗的錯誤很明顯,compareAndSet 計算失敗導致 執行緒2 沒有讓 i++ 那麼讓它重新執行咯 ———————————————————————————————————————————————————— 實驗3: while(true){ val current = i //設 i = 0 val next = current + 1 if(i.compareAndSet(current,next)){ //方法執行成功會返回 true,否則根本沒法判斷執行結果 break } } 執行假設: 1、執行緒1 計算出 current = 0 2、執行緒2 計算出 current = 0 3、執行緒1 計算出 next = 1 4、執行緒2 計算出 next = 1 5、執行緒1 執行 compareAndSet:i(0) == current(0),i = 1; if公式成立,執行緒1計算結束 6、執行緒2 執行 compareAndSet:i(1) != current(0); if公式失敗 7、執行緒1 執行 break 退出迴圈 8、執行緒2 進入第二次迴圈 此時 i = 1 9、執行緒2 計算出 current = 1 10、執行緒2 計算出 next = 2 11、執行緒2 執行 compareAndSet:i(1) == current(1),i = 2; if公式成立,執行緒2計算結束 12、最終運算得 i = 2 執行假設2: 1、執行緒1 計算出 current = 0 2、執行緒1 計算出 next = 1 3、執行緒1 執行 compareAndSet:i(0) == current(0),i = 1; if公式成立,執行緒1計算結束 4、執行緒2 計算出 current = 1 5、執行緒2 計算出 next = 2 6、執行緒2 執行 compareAndSet:i(1) == current(1),i = 2; if公式成立,執行緒2計算結束 7、最終運算得 i = 2 ———————————————————————————————————————————————————— 區區 while 和 val current = i 竟能解決執行緒的不靠譜!為什麼?1、有點像資料庫的同步塊,使用 while(true){} 包裹的同步快,一進入這個快,就意味著必須運算正確,否則要重新運算。 2、一旦執行 val current = i 就意味著運算開始,直到 compareAndSet 來得到運算是否正確 以及 保留正確的值。 3、整個運算從頭到尾,除了 i 沒有其它的變數干擾,所以運算結果一定是固定的(結果僅由i來決定)。 4、無論執行緒如何穿插執行,最後的 compareAndSet 限制死了“只有第一個跑到我這的(執行緒),才是運算成功的,其它的都回去重跑” “只有第一個跑到我這的,才是運算成功的,其它的都回去重跑”——使用 compareAndSet 的核心思想 */ //Kotlin 1.3 新特性,直接執行 main() //執行測試 fun main() { val i = AtomicInteger(0) //主角登場 //啟動4個執行緒 (0..3).forEach { j -> thread(name = "執行緒$j") { //每個執行緒計算1000次 (0 until 1000).forEach { while (true) { val current = i.get() // 通過 get() 來得到值 val next = current + 1 if (i.compareAndSet(current, next)) { //展示計算,當然可能順序不對,但最終結果一定正確 println("${Thread.currentThread().name} -> $i") break } } } } } }