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