計算機的 CPU、內存、I/O 設備的速度一直存在較大的差別,依次是 CPU > 內存 > I/O 設備,爲了權衡這三者的速度差別,主要提出了三種解決辦法:java
三種解決辦法雖然有效,可是也帶來了另外的三個問題,分別就是併發 bug 產生的源頭。緩存
1.可見性問題併發
若是是單核 CPU,多個線程操做的都是同一個 CPU 緩存,那麼一個線程修改了共享變量,另外一個線程確定能立刻看到。優化
若是是多核 CPU ,每一個 CPU 都有本身的緩存,這樣線程對共享變量的修改便對其餘線程不可見了。spa
2.原子性問題線程
爲何會有線程切換?一個線程在執行的過程當中,可能會進行耗時的 I/O 操做,這時線程須要等待 I/O 操做完成。線程在等待的過程當中,能夠釋放 CPU 的使用權,讓另外一個線程執行,這樣可以提升 CPU 的使用率。code
例如上圖,兩個線程同時對變量 count 加 1,線程 A 在執行的過程當中切換到了線程 B,最後致使寫入到內存的值都是 1,與預期不符。對象
3.有序性問題blog
首先看一段很經典的獲取單例對象的代碼:進程
public class Singleton { private static Singleton instance; //Java 獲取單例對象 public Singleton getInstance(){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(); } } } return instance; } }
程序的邏輯是:首先判斷 instance 是否爲空,若是爲空,對其加鎖,而後再判斷是否爲空,此時爲空的話則初始化 instance 對象。
若是線程 A 和 B 同時執行方法,在 synchronized 處,一個線程會被阻塞,假設被阻塞的是線程 B,此時線程 A 進入並初始化 instance,而後喚醒線程 B,線程 B 進入的時候,發現 instance 不爲空了,因此不會建立對象。
可是由於有序性問題的存在,這段代碼也不是想象的那麼完美,咱們指望的初始化對象的過程是這樣的:1.分配內存;2.初始化對象;3.將內存地址賦給 instance。可是通過編譯優化以後,倒是這樣的:
這樣,若是線程 A 執行到了第二步,而後切換到 線程 B,線程 B 就會認爲 instance 不爲空而後直接返回了,實際上 instance 並無初始化。
最後,總結一下,致使併發問題的三個源頭分別是