前言
- 先說(shuō)說(shuō)計(jì)算機(jī)緩存:計(jì)算機(jī)在執(zhí)行程序的時(shí)候,都是通過(guò)CPU來(lái)執(zhí)行指令,當(dāng)然執(zhí)行一串指令少不了需要某些數(shù)據(jù),這些數(shù)據(jù)就在主內(nèi)存中(物理內(nèi)存)。隨著科技不斷發(fā)展,CPU執(zhí)行速度越來(lái)越快,但內(nèi)存存取發(fā)展并沒(méi)有跟上CPU飛速發(fā)展的腳步,導(dǎo)致性能瓶頸出現(xiàn)在了內(nèi)存存取上,所以這個(gè)時(shí)候出現(xiàn)了緩存技術(shù)來(lái)加快數(shù)據(jù)的存取。
- 在程序真正運(yùn)行時(shí),會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀寫(xiě)數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。
- 但是當(dāng)出現(xiàn)多核CPU時(shí),每個(gè)核上都存在高速緩存,而且都可能運(yùn)行著線程,線程又是并發(fā)的,它們都會(huì)修改自己所在核的高速緩存中的變量,就會(huì)導(dǎo)致數(shù)據(jù)不一致的情況。
原子性、可見(jiàn)性、有序性
- 原子性是指在一個(gè)操作中就是cpu不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。 -- 處理器優(yōu)化會(huì)導(dǎo)致原子性問(wèn)題
- 可見(jiàn)性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。 -- 緩存一致性
- 有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。 -- 指令重排導(dǎo)致有序性問(wèn)題
- 如何實(shí)現(xiàn)上面說(shuō)到的三個(gè)特性呢?=>這就是Java的內(nèi)存模型要解決的問(wèn)題了
Java內(nèi)存模型 -- JMM
- Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是使到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存。
- 不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。所以,就可能出現(xiàn)線程1改了某個(gè)變量的值,但是線程2不可見(jiàn)的情況。
- 而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過(guò)程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步
- 所以,JMM就是一種規(guī)范,其用于解決由于多線程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題。
- Java內(nèi)存模型,除了定義了一套規(guī)范,還提供了一系列原語(yǔ)(volatile、synchronized、final、concurrent包),封裝了底層實(shí)現(xiàn)后,供開(kāi)發(fā)者直接使用。
volatile
- 使用volatile能將被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次使用之前都從主內(nèi)存刷新。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見(jiàn)性。
- volatile只能保證有序性和可見(jiàn)性。
synchronized
- synchronized可以保證原子性、有序性和可見(jiàn)性。
- 作用域:
- 作用于普通方法
- 作用于靜態(tài)方法
- 作用于代碼塊
- 只有在同步的塊或者方法中才能調(diào)用wait/notify等方法
- 作用范圍:
- 當(dāng)修飾方法時(shí)候,其只作用于本對(duì)象實(shí)例
- 當(dāng)修飾靜態(tài)方法時(shí),作用于該類(lèi)的對(duì)象(靜態(tài)方法本就屬于這個(gè)類(lèi),所以任何關(guān)于該類(lèi)的都可)
- 當(dāng)修飾代碼塊時(shí),還是該對(duì)象實(shí)例
- 這里再延伸探討一下synchronized是如何保證原子性的呢?這就要先說(shuō)說(shuō)Java虛擬機(jī)如何執(zhí)行線程同步了
Java虛擬機(jī)如何執(zhí)行線程同步
- 在Java的內(nèi)存結(jié)構(gòu)中,有兩塊區(qū)域會(huì)被所有的線程共享 -- 方法區(qū)(存儲(chǔ)靜態(tài)變量)和堆(存儲(chǔ)所有對(duì)象實(shí)例),因?yàn)閷?shí)現(xiàn)了共享所以同一個(gè)對(duì)象也應(yīng)該能被多個(gè)線程改變。要實(shí)現(xiàn)這些改變那就需要Java虛擬機(jī)來(lái)管控。
- 所以虛擬機(jī)給每個(gè)對(duì)象和類(lèi)都加了一個(gè)鎖,當(dāng)某個(gè)線程要去改變這個(gè)對(duì)象的時(shí)候,就會(huì)去向虛擬機(jī)申請(qǐng)鎖(虛擬機(jī)可能會(huì)延遲給鎖),獲取到鎖的線程對(duì)對(duì)象進(jìn)行修改,之后再將鎖還給虛擬機(jī),虛擬機(jī)又為下個(gè)線程分配鎖。
- 其中給類(lèi)加鎖,其實(shí)也是通過(guò)給對(duì)象加鎖實(shí)現(xiàn)的,因?yàn)槊總€(gè)類(lèi)加載的時(shí)候,都會(huì)有一個(gè)java.lang.Class的實(shí)例,給這個(gè)實(shí)例加鎖來(lái)實(shí)現(xiàn)。
- 監(jiān)視器(Monitors):JVM中鎖的是通過(guò)一個(gè)叫做監(jiān)視器的東西來(lái)實(shí)現(xiàn)的,監(jiān)視器用來(lái)監(jiān)視一段代碼,保證同一時(shí)間只有一個(gè)線程在執(zhí)行它。
- 每一個(gè)監(jiān)視器都與一個(gè)對(duì)段象相關(guān)聯(lián),當(dāng)開(kāi)始運(yùn)行到這段代碼時(shí),線程必須要獲取到它所引用對(duì)象的鎖。一旦獲得鎖,線程便可以進(jìn)入“被保護(hù)”的代碼開(kāi)始執(zhí)行。當(dāng)線程離開(kāi)代碼塊的時(shí)候,無(wú)論如何離開(kāi),都會(huì)釋放所關(guān)聯(lián)對(duì)象的鎖。
synchronized實(shí)現(xiàn)原理
- 每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過(guò)程如下:
- 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。
- 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。
- 如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
- 執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者。
- 指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè) monitor 的所有權(quán)。
- 這是synchronized同步代碼塊的原理,還有同步方法是通過(guò)ACC_SYNCHRONIZED標(biāo)志實(shí)現(xiàn)的,具體待補(bǔ)充...
最后回到synchronized是如何保證原子性問(wèn)題上
比如線程1在執(zhí)行monitorenter指令的時(shí)候,會(huì)對(duì)Monitor進(jìn)行加鎖,加鎖后其他線程無(wú)法獲得鎖,除非線程1主動(dòng)解鎖。即使在執(zhí)行過(guò)程中,由于某種原因,比如CPU時(shí)間片用完,線程1放棄了CPU,但是,他并沒(méi)有進(jìn)行解鎖。而由于synchronized的鎖是可重入的,下一個(gè)時(shí)間片還是只能被他自己獲取到,還是會(huì)繼續(xù)執(zhí)行代碼。直到所有代碼執(zhí)行完。這就保證了原子性。
參考文章
how-the-java-virtual-machine-performs-thread-synchronization
再有人問(wèn)你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他。
Synchronized的實(shí)現(xiàn)原理