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