1. 程式人生 > >傻瓜式的解答:為什麼原子運算 AtomicInteger 可以解決多執行緒計算臨界資源錯誤

傻瓜式的解答:為什麼原子運算 AtomicInteger 可以解決多執行緒計算臨界資源錯誤

/*
   為什麼多執行緒計算臨界資源會錯誤?
   原貼: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 } } } } } }