併發編程-可見性、順序性、原子性

一直以來,因爲cpu、內存、I/O存在着巨大的速度差別,cpu>內存>I/O。爲了平衡這三者的差別,計算機結構、操做系統、編譯器都作出了巨大的貢獻,主要體現爲:java

1. 增長cpu緩存,以均衡與內存的速度差別緩存

2. 編譯器優化指令執行次序,使得緩存可以更加合理利用多線程

3. 操做系統增長進程、線程,以分時複用cpu,進而均衡cpu與I/O的速度差別併發

以上的優化確實提升了cpu的使用率,提高了總體程序的性能,可是也給咱們的併發程序帶來了一些問題性能

1. cpu緩存致使的可見性問題優化

在單核時候,只有一個cpu以及一個cpu緩存,不一樣的線程對應的是同一個緩存,則不存在可見性問題。在多核時代,每顆cpu都有本身的緩存,不一樣的線程可能操做的是不一樣的緩存。例如線程 A操做的是cpu1的緩存,而線程B則操做的是cpu2的緩存,很明顯這個時候線程A對變量v的操做,對於線程B而言則是不可見的spa

2.  編譯優化帶來的有序性問題操作系統

編譯器爲了優化性能,有時候會改變語句的前後順序,這樣不但能夠更加合理的利用cpu緩存,同時能夠減小cpu沒必要要的停頓。而後編譯器的優化只能保證串行語義的一致,沒法保證多線程的語義也一致。在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的地址賦值給instance變量

3. 在內存M上初始化instance變量

我麼假設A線程執行getInstance方法,執行完指令2後,B線程開始執行,這時B發現第一個instance==null 爲false,直接返回了instance實例,然而此時instance實例尚未初始化完成,這個時候B線程訪問instance的成員變量就可能觸發空指針異常

3. 線程切換帶來的原子性問題

線程的切換是由操做系統來處理的,而操做系統走切換是可以發生在任何一條cpu執行完成的。而不少高級語言的一條語句對應cpu指令,count++這個操做則須要三條指令

  • 指令1:將count從內存load到cpu寄存器中
  • 指令2:將寄存器中的count進行+1
  • 指令3:將count寫入內存

當線程A執行完指令1後,切換到線程B則發生原子性問題

相關文章
相關標籤/搜索