CPU和緩存一致性
我們應(yīng)該都知道,計(jì)算機(jī)在執(zhí)行程序的時(shí)候,每條指令都是在CPU中執(zhí)行的,而執(zhí)行的時(shí)候,又免不了要和數(shù)據(jù)打交道。而計(jì)算機(jī)上面的數(shù)據(jù),是存放在主存當(dāng)中的,也就是計(jì)算機(jī)的物理內(nèi)存啦。
隨著CPU技術(shù)的發(fā)展,CPU的執(zhí)行速度越來(lái)越快。而由于內(nèi)存的技術(shù)并沒有太大的變化,所以從內(nèi)存中讀取和寫入數(shù)據(jù)的過程和CPU的執(zhí)行速度比起來(lái)差距就會(huì)越來(lái)越大,這就導(dǎo)致CPU每次操作內(nèi)存都要耗費(fèi)很多等待時(shí)間。
人們想出來(lái)了一個(gè)好的辦法,就是在CPU和內(nèi)存之間增加高速緩存。緩存的概念大家都知道,就是保存一份數(shù)據(jù)拷貝。他的特點(diǎn)是速度快,內(nèi)存小,并且昂貴。
那么,程序的執(zhí)行過程就變成了:?
當(dāng)程序在運(yùn)行過程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。
而隨著CPU能力的不斷提升,一層緩存就慢慢的無(wú)法滿足要求了,就逐漸的衍生出多級(jí)緩存。
那么,在有了多級(jí)緩存之后,程序的執(zhí)行就變成了:?
當(dāng)CPU要讀取一個(gè)數(shù)據(jù)時(shí),首先從一級(jí)緩存中查找,如果沒有找到再?gòu)亩?jí)緩存中查找,如果還是沒有就從三級(jí)緩存或內(nèi)存中查找。
下圖為一個(gè)單CPU雙核的緩存結(jié)構(gòu)

我們分別來(lái)分析下單線程、多線程在單核CPU、多核CPU中的影響。
單核CPU,單線程。cpu核心的緩存只被一個(gè)線程訪問。緩存獨(dú)占,不會(huì)出現(xiàn)訪問沖突等問題。
單核CPU,多線程。進(jìn)程中的多個(gè)線程會(huì)同時(shí)訪問進(jìn)程中的共享數(shù)據(jù),CPU將某塊內(nèi)存加載到緩存后,不同線程在訪問相同的物理地址的時(shí)候,都會(huì)映射到相同的緩存位置,這樣即使發(fā)生線程的切換,緩存仍然不會(huì)失效。但由于任何時(shí)刻只能有一個(gè)線程在執(zhí)行,因此不會(huì)出現(xiàn)緩存訪問沖突。
多核CPU,多線程。每個(gè)核都至少有一個(gè)L1 緩存。多個(gè)線程訪問進(jìn)程中的某個(gè)共享內(nèi)存,且這多個(gè)線程分別在不同的核心上執(zhí)行,則每個(gè)核心都會(huì)在各自的caehe中保留一份共享內(nèi)存的緩沖。由于多核是可以并行的,可能會(huì)出現(xiàn)多個(gè)線程同時(shí)寫各自的緩存的情況,而各自的cache之間的數(shù)據(jù)就有可能不同。
在CPU和主存之間增加緩存,在多線程場(chǎng)景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個(gè)核的自己的緩存中,關(guān)于同一個(gè)數(shù)據(jù)的緩存內(nèi)容可能不一致。
處理器優(yōu)化和指令重排
上面提到在在CPU和主存之間增加緩存,在多線程場(chǎng)景下會(huì)存在緩存一致性問題。除了這種情況,還有一種硬件問題也比較重要。那就是為了使處理器內(nèi)部的運(yùn)算單元能夠盡量的被充分利用,處理器可能會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行處理。這就是處理器優(yōu)化。
除了現(xiàn)在很多流行的處理器會(huì)對(duì)代碼進(jìn)行優(yōu)化亂序處理,很多編程語(yǔ)言的編譯器也會(huì)有類似的優(yōu)化,比如Java虛擬機(jī)的即時(shí)編譯器(JIT)也會(huì)做指令重排。
可想而知,如果任由處理器優(yōu)化和編譯器對(duì)指令重排的話,就可能導(dǎo)致各種各樣的問題。
并發(fā)編程的問題?
上面說的和硬件有關(guān)的概念可能聽得有點(diǎn)蒙,還不知道他到底和軟件有啥關(guān)系。但是關(guān)于并發(fā)編程的問題應(yīng)該有所了解,比如原子性問題,可見性問題和有序性問題。
其實(shí),原子性問題,可見性問題和有序性問題。是人們抽象定義出來(lái)的。而這個(gè)抽象的底層問題就是前面提到的緩存一致性問題、處理器優(yōu)化問題和指令重排問題等。
我們說,并發(fā)編程,為了保證數(shù)據(jù)的安全,需要滿足以下三個(gè)特性:?
原子性是指在一個(gè)操作中就是cpu不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。?
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。?
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。?
有沒有發(fā)現(xiàn),緩存一致性問題其實(shí)就是可見性問題。而處理器優(yōu)化是可以導(dǎo)致原子性問題的。指令重排即會(huì)導(dǎo)致有序性問題。所以,后文將不再提起硬件層面的那些概念,而是直接使用大家熟悉的原子性、可見性和有序性。
什么是內(nèi)存模型
上面提到的,緩存一致性問題、處理器器優(yōu)化的指令重排問題是硬件的不斷升級(jí)導(dǎo)致的。那么,有沒有什么機(jī)制可以很好的解決上面的這些問題呢??
最簡(jiǎn)單直接的做法就是廢除處理器和處理器的優(yōu)化技術(shù)、廢除CPU緩存,讓CPU直接和主存交互。但是,這么做雖然可以保證多線程下的并發(fā)問題。但是,這就有點(diǎn)因噎廢食了。?
所以,為了保證并發(fā)編程中可以滿足原子性、可見性及有序性。有一個(gè)重要的概念,那就是——內(nèi)存模型。?
為了保證共享內(nèi)存的正確性(可見性、有序性、原子性),內(nèi)存模型定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范。通過這些規(guī)則來(lái)規(guī)范對(duì)內(nèi)存的讀寫操作,從而保證指令執(zhí)行的正確性。它與處理器有關(guān)、與緩存有關(guān)、與并發(fā)有關(guān)、與編譯器也有關(guān)。他解決了CPU多級(jí)緩存、處理器優(yōu)化、指令重排等導(dǎo)致的內(nèi)存訪問問題,保證了并發(fā)場(chǎng)景下的一致性、原子性和有序性。?
內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
什么是Java內(nèi)存模型
計(jì)算機(jī)內(nèi)存模型,是解決多線程場(chǎng)景下并發(fā)問題的一個(gè)重要規(guī)范。
Java程序是需要運(yùn)行在Java虛擬機(jī)上面的,Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。不同的線程之間也無(wú)法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。
而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步。

JMM是一種規(guī)范,目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問題。
Java內(nèi)存模型的實(shí)現(xiàn)
Java提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如volatile、synchronized、final等。其實(shí)這些就是Java內(nèi)存模型封裝了底層的實(shí)現(xiàn)后提供給程序員使用的一些關(guān)鍵字。?
在開發(fā)多線程的代碼的時(shí)候,我們可以直接使用synchronized等關(guān)鍵字來(lái)控制并發(fā),從來(lái)就不需要關(guān)心底層的編譯器優(yōu)化、緩存一致性等問題。所以,Java內(nèi)存模型,除了定義了一套規(guī)范,還提供了一系列原語(yǔ),封裝了底層實(shí)現(xiàn)后,供開發(fā)者直接使用。
我們前面提到,并發(fā)編程要解決原子性、有序性和一致性的問題,我們就再來(lái)看下,在Java中,分別使用什么方式來(lái)保證。
原子性
在Java中,為了保證原子性,提供了兩個(gè)高級(jí)的字節(jié)碼指令monitorenter和monitorexit。?
因此,在Java中可以使用synchronized來(lái)保證方法和代碼塊內(nèi)的操作是原子性的。
可見性
Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)的。?
Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見性。?
除了volatile,Java中的synchronized和final兩個(gè)關(guān)鍵字也可以實(shí)現(xiàn)可見性,不過實(shí)現(xiàn)方式不同。
有序性
在Java中,可以使用synchronized和volatile來(lái)保證多線程之間操作的有序性。實(shí)現(xiàn)方式有所區(qū)別:?
volatile關(guān)鍵字會(huì)禁止指令重排。synchronized關(guān)鍵字保證同一時(shí)刻只允許一條線程操作。?
讀者可能發(fā)現(xiàn)了,好像synchronized關(guān)鍵字是萬(wàn)能的,他可以同時(shí)滿足以上三種特性,這其實(shí)也是很多人濫用synchronized的原因。?
但是synchronized是比較影響性能的,雖然編譯器提供了很多鎖優(yōu)化技術(shù),但是也不建議過度使用。