三道java新手入門面試題,通往自由的道路--JVM
在Java的并發(fā)中采用的就是JVM內(nèi)存共享模型即JMM(Java Memory Model),它其實(shí)是是JVM規(guī)范中所定義的一種內(nèi)存模型,跟計(jì)算機(jī)的CPU緩存內(nèi)存模型類似,是基于CPU緩存內(nèi)存模型來(lái)建立的,Java內(nèi)存模型是標(biāo)準(zhǔn)化的,屏蔽掉了底層不同計(jì)算機(jī)的區(qū)別。
那我們先來(lái)講下計(jì)算機(jī)的內(nèi)存模型:
其實(shí)早期計(jì)算機(jī)中CPU和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中,CPU的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來(lái)作為內(nèi)存與處理器之間的緩沖。
將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫了。
基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來(lái)更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問(wèn)題:緩存一致性(CacheCoherence)。
在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。
而我們可以打開任務(wù)管理器,可以進(jìn)入性能 --> CPU中可以看到L1緩存、L2緩存和L3緩存。
可以看到我們CPU跟我們計(jì)算機(jī)之間交互的高速緩存。一般的流程,就是計(jì)算機(jī)會(huì)先從硬盤從讀取數(shù)據(jù)到主內(nèi)存中,又會(huì)從主內(nèi)存讀取數(shù)據(jù)到高速緩存中,而CPU讀取的數(shù)據(jù)就是高速緩存中的數(shù)。
我們現(xiàn)在再來(lái)看看JMM:
JMM是定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存在主內(nèi)存(MainMemory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(LocalMemory)即共享變量副本,本地內(nèi)存中存儲(chǔ)了該線程以讀、寫共享變量的副本。本地內(nèi)存是Java內(nèi)存模型的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器等。
JMM模型圖:
我們可以發(fā)現(xiàn)在JMM模型中:
所有的共享變量都存在主內(nèi)存中。 每個(gè)線程都保存了一份該線程使用到的共享變量的副本。 線程A是無(wú)法直接訪問(wèn)到線程B的本地內(nèi)存的,只能訪問(wèn)主內(nèi)存。線 程對(duì)共享變量的所有操作都必須在自己的本地內(nèi)存中進(jìn)行,不能直接從主內(nèi)存中讀取。 并發(fā)的三要素:可見性、原子性、有序性,而JMM就主要體現(xiàn)在這三方面。注意 :因?yàn)榫€程之間無(wú)法相互訪問(wèn),而一旦某個(gè)線程將共享變量進(jìn)行修改,而線程B是無(wú)法發(fā)現(xiàn)到這個(gè)更新值的,所以可能會(huì)出現(xiàn)可見性問(wèn)題。而這里的可見性問(wèn)題就是一個(gè)線程對(duì)共享變量的修改,另一個(gè)線程能夠立刻看到,但此時(shí)無(wú)法看到更新后的內(nèi)存,因?yàn)樵L問(wèn)的是自己的共享變量副本。
解決方案有
加鎖,加synchronized、Lock,保存一個(gè)線程只能等另一個(gè)線程結(jié)束后才能再訪問(wèn)變量。 對(duì)共享變量加上volatile關(guān)鍵字,保證了這個(gè)變量是可見的。2. 你知道重排序是什么嗎?重排序是指計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排。
首先我們來(lái)看看為什么指令重排序可以提高性能?
每一個(gè)指令都會(huì)包含多個(gè)步驟,每個(gè)步驟可能使用不同的硬件,而現(xiàn)代處理器會(huì)設(shè)計(jì)為一個(gè)時(shí)鐘周期完成一條執(zhí)行時(shí)間最長(zhǎng)的指令,為什么會(huì)這樣呢?
主要原理就是可以指令1還沒(méi)有執(zhí)行完,就可以開始執(zhí)行指令2,而不用等到指令1執(zhí)行結(jié)束之后再執(zhí)行指令2,這樣就大大提高了效率。
例如:每條指令拆分為五個(gè)階段:
想這樣如果是按順序串行執(zhí)行指令,那可能相對(duì)比較慢,因?yàn)樾枰却弦粭l指令完成后,才能等待下一步執(zhí)行:
而如果發(fā)生指令重排序呢,實(shí)際上雖然不能縮短單條指令的執(zhí)行時(shí)間,但是它變相地提高了指令的吞吐量,可以在一個(gè)時(shí)鐘周期內(nèi)同時(shí)運(yùn)行五條指令的不同階段。
我們來(lái)分析下代碼的執(zhí)行情況,并思考下:
a = b + c;
d = e - f ;
按原先的思路,會(huì)先加載b和c,再進(jìn)行b+c操作賦值給a,接下來(lái)就會(huì)加載e和f,最后就是進(jìn)行e-f操作賦值給d。
這里有什么優(yōu)化的空間呢?我們?cè)趫?zhí)行b+c操作賦值給a時(shí),可能需要等待b和c加載結(jié)束,才能再進(jìn)行一個(gè)求和操作,所以這里可能出現(xiàn)了一個(gè)停頓等待時(shí)間,依次后面的代碼也可能會(huì)出現(xiàn)停頓等待時(shí)間,這降低了計(jì)算機(jī)的執(zhí)行效率。
為了去減少這個(gè)停頓等待時(shí)間,我們可以先加載e和f,然后再去b+c操作賦值給a,這樣做對(duì)程序(串行)是沒(méi)有影響的,但卻減少了停頓等待時(shí)間。既然b+c操作賦值給a需要停頓等待時(shí)間,那還不如去做一些有意義的事情。
總結(jié):指令重排對(duì)于提高CPU處理性能十分必要。雖然由此帶來(lái)了亂序的問(wèn)題,但是這點(diǎn)犧牲是值得的。
重排序的類型有以下幾種:
指令重排一般分為以下三種:
編譯器優(yōu)化重排編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
指令并行重排現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個(gè)執(zhí)行的語(yǔ)句無(wú)需依賴前面執(zhí)行的語(yǔ)句的結(jié)果),處理器可以改變語(yǔ)句對(duì)應(yīng)的機(jī)器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)重排由于處理器使用緩存和讀寫緩存沖區(qū),這使得加載(load)和存儲(chǔ)(store)操作看上去可能是在亂序執(zhí)行,因?yàn)槿?jí)緩存的存在,導(dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時(shí)間差。
而在重排序中還需要一個(gè)概念的東西:as-if-serial
不管如何重排序,都必須保證代碼在單線程下的運(yùn)行正確,連單線程下都無(wú)法正確,更不用討論多線程并發(fā)的情況,所以就提出了一個(gè)as-if-serial的概念。
as-if-serial語(yǔ)義的意思是:
不管怎么重排序,程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。 為了遵守as-if-serial語(yǔ)義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。(強(qiáng)調(diào)一下,這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮)。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作依然可能被編譯器和處理器重排序。3. happens-before是什么,和as-if-serial有什么區(qū)別happens-before的概念:
一方面,程序員需要JMM提供一個(gè)強(qiáng)的內(nèi)存模型來(lái)編寫代碼;另一方面,編譯器和處理器希望JMM對(duì)它們的束縛越少越好,這樣它們就可以最可能多的做優(yōu)化來(lái)提高性能,希望的是一個(gè)弱的內(nèi)存模型。
JMM考慮了這兩種需求,并且找到了平衡點(diǎn),對(duì)編譯器和處理器來(lái)說(shuō),只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確同步了的多線程程序),編譯器和處理器怎么優(yōu)化都行。
而對(duì)于程序員,JMM提供了happens-before規(guī)則(JSR-133規(guī)范),在JMM中,如果一個(gè)線程執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作進(jìn)行可見,那么這兩個(gè)操作直接必須存在happens-before關(guān)系。
JMM使用happens-before的概念來(lái)定制兩個(gè)操作之間的執(zhí)行順序。這并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見,且前一個(gè)操作按順序排在第二個(gè)操作之前 。
happens-before關(guān)系的定義如下:
如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來(lái)執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么JMM也允許這樣的重排序。 happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被重排序改變。在Java中,有以下天然的Happens-Before規(guī)則:
程序順序規(guī)則:一個(gè)線程中的每一個(gè)操作,happens-before于該線程中的任意后續(xù)操作。 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。 volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。 start規(guī)則:如果線程A執(zhí)行操作ThreadB.start()啟動(dòng)線程B,那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作、 join規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。 線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用happens-before于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生。Happens-Before和as-if-serial的關(guān)系實(shí)質(zhì)上是一回事。
as-if-serial語(yǔ)義保證單線程內(nèi)重排序后的執(zhí)行結(jié)果和程序代碼本身應(yīng)有的結(jié)果是一致的,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被重排序改變。 as-if-serial語(yǔ)義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。總結(jié)這篇文章就到這里了,如果這篇文章對(duì)你也有所幫助,希望您能多多關(guān)注好吧啦網(wǎng)的更多內(nèi)容!
相關(guān)文章:
