如何解決併發問題,首先要理解併發問題的實際源頭怎麼發生的。編程
現代計算機的不一樣硬件的運行速度是差別很大的,這個你們應該都是知道的。緩存
計算機數據傳輸運行速度上的快慢比較:
CPU > 緩存 > I/O
如何最大化的讓不一樣速度的硬件能夠更好的協調執行,須要作一些「撮合」的工做併發
上面說來併發才生問題的背景,下面說下併發產生的具體緣由是什麼編程語言
先看下單核CPU和緩存之間的關係:
性能
單核狀況下,也是最簡單的狀況,線程A操做寫入變量A,這個變量A的值確定是被線程B所見的。由於2個線程是在一個CPU上操做,所用的也是同一個CPU緩存。優化
這裏咱們來定義spa
一個線程對共享變量的修改,另一個線程可以馬上看到,咱們稱爲 「可見性」
多核CPU時代下,咱們在來看下具體狀況:操作系統
很明顯,多核狀況下每一個CPU都有本身的高速緩存,因此變量A的在每一個CPU中多是不一樣步的,不一致的。
結果程A恰好操做來CPU1的緩存,而線程B也恰好只操做了CPU2的緩存。因此這狀況下,當線程A操做變量A的時候,變量並不對線程B可見。線程
咱們用一段經典的代碼說明下可見性的問題:code
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,編譯帶來的有序性問題
深入理解這些來龍去脈,能夠診斷大部分併發的問題!