摘要:所以這情況下,當線程操作變量的時候,變量并不對線程可見。總結,緩存引發的可見性問題,切換線程帶來的原子性問題,編譯帶來的有序性問題深刻理解這些前因后果,可以診斷大部分并發的問題
背景介紹
如何解決并發問題,首先要理解并發問題的實際源頭怎么發生的。
現代計算機的不同硬件的運行速度是差異很大的,這個大家應該都是知道的。
計算機數據傳輸運行速度上的快慢比較:
CPU > 緩存 > I/O
如何最大化的讓不同速度的硬件可以更好的協調執行,需要做一些“撮合”的工作
CUP增加了高速緩存來均衡與緩存間的速度差異
操作系統增加了 進程,線程,以分時復用CPU,進而均衡CPU與I/O的速度差異(當等待I/O的時候系統切換CPU給系統程序使用)
現代編程語言的編譯器優化指令順序,使得緩存能夠合理的利用
上面說來并發才生問題的背景,下面說下并發產生的具體原因是什么
并發產生的原因 緩存導致的可見性問題先看下單核CPU和緩存之間的關系:
單核情況下,也是最簡單的情況,線程A操作寫入變量A,這個變量A的值肯定是被線程B所見的。因為2個線程是在一個CPU上操作,所用的也是同一個CPU緩存。
這里我們來定義
一個線程對共享變量的修改,另外一個線程能夠立刻看到,我們稱為 “可見性”
多核CPU時代下,我們在來看下具體情況:
很明顯,多核情況下每個CPU都有自己的高速緩存,所以變量A的在每個CPU中可能是不同步的,不一致的。
結果程A剛好操作來CPU1的緩存,而線程B也剛好只操作了CPU2的緩存。所以這情況下,當線程A操作變量A的時候,變量并不對線程B可見。
我們用一段經典的代碼說明下可見性的問題:
private void add10K() { int idx = 0; while (idx++ < 100000) { count += 1; } } @Test public void demo() { // 創建兩個線程,執行 add() 操作 Thread th1 = new Thread(() -> { add10K(); }); Thread th2 = new Thread(() -> { add10K(); }); // 啟動兩個線程 th1.start(); th2.start(); // 等待兩個線程執行結束 try { th1.join(); th2.join(); } catch (Exception exc) { exc.printStackTrace(); } System.out.println(count); }
大家應該都知道,答案肯定不是 200000
這就是可見性導致的問題,因為2個線程讀取變量count時,讀取的都是自己CPU下的高速緩存內的緩存值,+1時也是在自己的高速緩存中。
進程切換最早是為了提高CPU的使用率而出現的。
比如,50毫米操作系統會重新選擇一個進程來執行(任務切換),50毫米成為“時間片”
早期的操作系統是進程間的切換,進程間的內存空間是不共享的,切換需要切換內存映射地址,切換成本大。
而一個進程創建的所有線程,內存空間都是共享的。所以現在的操作系統都是基于更輕量的線程實現切換的,現在我們提到的“任務切換”都是線程切換。
任務切換的時機大多數在“時間片”結束的時候。
現在我們使用的基本都是高級語言,高級語言的一句對應多條CPU命令,比如 count +=1 至少對應3條CPU命令,指令:
1, 從內存加載到CPU的寄存器
2, 在寄存器執行 +1
3, 最后,講結果寫回內存(緩存機制導致可能寫入的是CPU緩存而不是內存)
操作系統做任務切換,會在 任意一條CPU指令執行完就行切換。所以會導致問題
如圖所示,線程A當執行完初始化count=0時候,剛好被線程切換給了線程B。線程B執行count+1=1并最后寫入值到緩存中,CPU切換回線程A后,繼續執行A線程的count+1=1并再次寫入緩存,最后緩存中的count還是為1.
一開始我們任務count+1=1應該是一個不能再被拆開的原子操作。
我們把一個或多個操作在CPU執行過程中的不被中斷的特性稱為 原子性。
CPU能夠保證的原子性,是CPU指令級別的。所以高級語言需要語言層面 保證操作的原子性。
編譯優化帶來的有序性問題有序性。顧名思義,有序性指的是程序按照代碼的先后順序執行。
編譯器為了優化性能,有時候會改變程序中語句的先后順序,例如程序中:a=6;b=7;編譯器優化后可能變成b=7;a=6;,在這個例子中,編譯器調整了語句的順序,但是不影響程序的最終結果。不過有時候編譯器及解釋器的優化可能導致意想不到的 Bug。
Java中的經典案例,雙重檢查創建單例對象;
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
看似完美的代碼,其實有問題。問題就在new上。
想象中 new操作步驟:
1,分配一塊內存 M
2,在內存M上 初始化對象
3,把內存M地址賦值給 變量
實際上就行編譯后的順序是:
1,分開一塊內存 M
2,把內存M地址賦值給 變量
3,在 內存M上 初始化對象
優化導致的問題:
如圖所示,當線程A執行到第二步的時候,被線程切換了,這時候,instance未初始化實例的對象,而線程B這時候執行到instance == null ?的判斷中,發現instance已經有“值”了,導致了返回了一個空對象的異常。
總結1,緩存引發的可見性問題
2,切換線程帶來的原子性問題
3,編譯帶來的有序性問題
深刻理解這些前因后果,可以診斷大部分并發的問題!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73764.html
摘要:最后,總結一下,導致并發問題的三個源頭分別是原子性一個線程在執行的過程當中不被中斷。可見性一個線程修改了共享變量,另一個線程能夠馬上看到,就叫做可見性。 計算機的 CPU、內存、I/O 設備的速度一直存在較大的差異,依次是 CPU > 內存 > I/O 設備,為了權衡這三者的速度差異,主要提出了三種解決辦法: CPU 增加了緩存,均衡和內存的速度差異 發明了進程、線程,分時復用 CP...
摘要:因為多線程競爭鎖時會引起上下文切換。減少線程的使用。舉個例子如果說服務器的帶寬只有,某個資源的下載速度是,系統啟動個線程下載該資源并不會導致下載速度編程,所以在并發編程時,需要考慮這些資源的限制。 最近私下做一項目,一bug幾日未解決,總惶恐。一日頓悟,bug不可怕,怕的是項目不存在bug,與其懼怕,何不與其剛正面。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Jav...
摘要:此時線程和會再有一個線程能夠獲取寫鎖,假設是,如果不采用再次驗證的方式,此時會再次查詢數據庫。而實際上線程已經把緩存的值設置好了,完全沒有必要再次查詢數據庫。 大家知道了Java中使用管程同步原語,理論上可以解決所有的并發問題。那 Java SDK 并發包里為什么還有很多其他的工具類呢?原因很簡單:分場景優化性能,提升易用性 今天我們就介紹一種非常普遍的并發場景:讀多寫少場景。實際工作...
閱讀 2632·2021-10-14 09:47
閱讀 4909·2021-09-22 15:52
閱讀 3355·2019-08-30 15:53
閱讀 1428·2019-08-30 15:44
閱讀 669·2019-08-29 16:41
閱讀 1646·2019-08-29 16:28
閱讀 439·2019-08-29 15:23
閱讀 1618·2019-08-26 12:20