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