一、并發編程bug的源頭(可見性、原子性、有序性)
CPU、內存、I/O設備的訪問速度差異大,為提高計算機性能的利用,計算機做了以下三點:
1.CPU增加了緩存,平衡與內存差異。
2.操作系統增加了進程、線程、分時復用CPU,進而均衡CPU與I/O設備的速度差異。
3.編譯程序優化指令執行順序,使得緩存能夠更加合理的利用同樣的,這也為并發程序帶來了三個問題:可見性、原子性、有序性。
可見性:一個線程對共享變量的修改,對另一個線程可見
現在的計算機處于多核時代,每顆CPU都有自己的緩存,這樣與內存就帶有數據不一致的問題,當線程A在CPU1將變量帶入緩存進行+1,同時,線程B在CPU2也將變量讀入緩存進行+1,我們的預期是變量+2,但最終的結果是變量+1。這就是沒有考慮可見性帶來的bug。
原子性:一個或者多個操作在CPU內不被中斷的特性。
操作系統有了多線程,同時支持分時復用,一個進程在CPU執行一個時間片,時間片到點,記錄數據,切換線程。這就帶來了原子性的問題。線程A讀取變量到緩存中,但這時CPU切換內存,線程A被阻塞,線程B讀取變量,并修改了變量的值,然后喚醒了線程A,線程A并不知道變量已經被修改,仍舊繼續執行修改變量操作,出現bug。
有序性:程序代碼按照代碼的先后順序執行
編譯器為了增加性能,有時優化代碼的同時會改變代碼的執行順序,在單一線程這或許沒有什么問題,但在并發的條件下這就有可能帶來問題。比如線程A創建一個對象,對象的new操作在我們的理解是:1分配一個內存M,2在M中初始化對象,3將M的地址賦予變量,但編譯器優化后順序會變成:1分配一個內存M,2將M的地址賦予變量,3在M中初始化對象。倘若線程A運行完第二步,切換到線程B,線程B看到對象已經被創建(實際上只是分配地址),對對象進行運算,出現BUG。(這里有可能對有序性和原子性產生疑惑,若編譯器沒有優化,線程A執行完第二步,變量還是沒有分配地址,那即使切換到線程B也不會對變量進行操作)
二、Java內存模型(解決可見性、有序性)
可見性是緩存帶來的問題,有序性是編譯優化帶來的問題,解決它們的方法是禁用它們,但這會讓性能下降,失去了并發的意義。這就需要內存模型
Java內存模型是一些很復雜的規范。簡單說,規范了JVM如何按需禁用緩存和編譯優化的方法。這些方法有violatile,synchronized和final,以及六項Happens-Before規范。
synchronized可以修飾代碼塊,方法。(理論篇)
final修飾的變量為常量,可以讓編譯器盡情優化,在1.5之后只要我們提供正確的構造函數不造成“逸出”,final常量就不會出什么問題。
(逸出:構造函數初始化還沒完成就將對象賦予別人)
被violate修飾的變量值,是禁用緩存的,即變量的修改只能從內存層面上進行。但同樣會帶來一個問題,我不能什么變量都使用violate修飾,這樣就會失去緩存的意義。
同時violate修飾的變量也會帶來一個問題那就是可見性問題。(不是指被修飾的變量有可見性問題)。