介紹
為了屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果,java虛擬機(jī)規(guī)范中定義了java內(nèi)存模型,簡(jiǎn)稱JMM。java內(nèi)存模型規(guī)范了java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的,它規(guī)定了一個(gè)線程如何和何時(shí)可以看到其他線程修改過的共享變量的值,以及在必須時(shí)如何同步地訪問共享變量。
一、jvm內(nèi)存分配結(jié)構(gòu)
- java中的堆是運(yùn)行時(shí)的數(shù)據(jù)區(qū)域,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)的分配內(nèi)存大小,生存期也不必事先告訴編譯器,垃圾回收機(jī)制負(fù)責(zé)回收不再使用的數(shù)據(jù),缺點(diǎn)是由于運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,因此存取速度相對(duì)慢一些。
- 棧的優(yōu)勢(shì)是存取數(shù)據(jù)比堆要快一些,僅次于計(jì)算中的寄存器,棧的數(shù)據(jù)是可以共享的。缺點(diǎn)是存在棧中的數(shù)據(jù)的生存期以及大小必須是確定的,缺乏一些靈活性。棧中主要存放一些基本類型的變量。
- java內(nèi)存模型要求調(diào)用棧和本地變量存放在線程棧上,對(duì)象存放在堆上。具體說下,一個(gè)本地變量,也有可能指向一個(gè)對(duì)象的引用,這種情況下,引用這個(gè)本地變量是存放在線程棧上,但是對(duì)象本身是存放在堆上的。一個(gè)對(duì)象包含方法,方法中的本地變量是存放在線程棧上的,即使這些方法所屬的對(duì)象存放在堆上。一個(gè)對(duì)象的成員變量可能隨著對(duì)象自身存放在堆上,不管這個(gè)成員變量是原始類型還是引用類型。靜態(tài)成員變量跟隨著類的定義一起存放在堆上,存在堆上的對(duì)象可以被所持有這個(gè)對(duì)象引用的線程訪問。
- 當(dāng)一個(gè)線程可以訪問這個(gè)對(duì)象的時(shí)候,它也可以訪問這個(gè)對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法,他們就會(huì)都訪問這個(gè)對(duì)象的成員變量,但是每一個(gè)線程都擁有了這個(gè)成員變量的私有拷貝
二、多核并發(fā)緩存架構(gòu)
-
計(jì)算機(jī)在寄存器上執(zhí)行的速度是遠(yuǎn)大于在主內(nèi)存上執(zhí)行的速度;
-
由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度之間存在幾個(gè)數(shù)量級(jí)的差距,所以新的計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度都盡可能接近處理器運(yùn)算速度的高級(jí)緩存來作為內(nèi)存與處理器之間的緩沖,將運(yùn)算使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算快速執(zhí)行,當(dāng)運(yùn)算結(jié)束后,再將數(shù)據(jù)從緩存同步到內(nèi)存中,這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫了。
-
一個(gè)計(jì)算機(jī)還包含一個(gè)主存,所有的cpu都可以訪問這個(gè)主存,主存通常比CPU中的緩存大得多。
-
多核cpu運(yùn)作原理:
通常情況下,當(dāng)一個(gè)CPU需要讀取主存的時(shí)候,它會(huì)將主存的數(shù)據(jù)讀取到CPU緩存中,甚至?xí)⒕彺嬷械牟糠謨?nèi)容讀到它內(nèi)部的寄存器里面,然后在寄存器中執(zhí)行操作;當(dāng)CPU需要將結(jié)果回寫到主存的時(shí)候,它會(huì)將內(nèi)部寄存器的值刷新到緩存中,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存。
jvm內(nèi)存分配結(jié)構(gòu)與cpu多核并發(fā)緩存架構(gòu)直接的關(guān)聯(lián)
- 硬件架構(gòu)沒有區(qū)分線程棧和堆,對(duì)于硬件架構(gòu)來說所有的線程棧和堆都分布在主內(nèi)存中,部分的線程棧和堆可能有時(shí)候會(huì)出現(xiàn)在CPU緩存中,和CPU內(nèi)部的寄存器里面
三、java內(nèi)存模型
- 線程之間共享變量存儲(chǔ)在主內(nèi)存中,每一個(gè)線程都有一個(gè)私有的本地內(nèi)存(工作內(nèi)存),本地內(nèi)存是java內(nèi)存模型的一個(gè)抽象的概念,不是真實(shí)存在的,它涵蓋了緩存,寫緩沖區(qū),寄存器,以及其他的硬件和編譯器的優(yōu)化。本地內(nèi)存中它存儲(chǔ)了該線程以讀或?qū)懝蚕碜兞康目截惖囊粋€(gè)副本。
- 從更低的角度說,主內(nèi)存就是硬件的內(nèi)存,是為了獲取更好的運(yùn)行速度,虛擬機(jī)以及硬件系統(tǒng)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中。
- java內(nèi)存模型中線程的工作內(nèi)存是CPU的寄存器和高速緩存的一個(gè)抽象的描述,而JVM的靜態(tài)內(nèi)存存儲(chǔ)模型(jvm內(nèi)存模型),它只是一種對(duì)內(nèi)存的物理化分而已,它只局限在JVM的內(nèi)存。
- 線程A和線程B通信必須要經(jīng)過下面兩個(gè)步驟:
- 線程A需要將本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存里面;
- 線程B去主內(nèi)存中讀取線程A之前更新過的共享變量
- Java的多線程之間是通過共享內(nèi)存進(jìn)行通信的,而由于采用共享內(nèi)存進(jìn)行通信,在通信過程中會(huì)存在一系列如可見性、原子性、順序性等問題,而JMM就是圍繞著多線程通信以及與其相關(guān)的一系列特性而建立的模型。JMM定義了一些語(yǔ)法集,這些語(yǔ)法集映射到Java語(yǔ)言中就是volatile、synchronized等關(guān)鍵字。
四、java內(nèi)存模型同步的八種指令操作
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量傳遞給執(zhí)行引擎。每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到的變量的值得字節(jié)碼指令時(shí),就會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量。每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)會(huì)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳送的主內(nèi)存中,以便隨后的write的操作
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
對(duì)應(yīng)的java內(nèi)存模型的同步規(guī)則:
- 如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順序地執(zhí)行read和load操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要順序地執(zhí)行store和write操作。但java內(nèi)存模型只要求上述操作必須順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn)。
- 不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
- 不允許一個(gè)線程無(wú)原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過了assign和load操作。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)。
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中次變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)他執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。
五、案列分析
下面我們結(jié)合java內(nèi)存模型分析下共享變量執(zhí)行i++的操作流程:
- 初始化共享變量i=0。
- 線程1通過read指令從主內(nèi)存中讀取出共享變量i=0,通過load指令加載到線程1的工作內(nèi)存中。
- 在線程1的工作內(nèi)存中,通過use指令將共享變量i=0加載到cpu執(zhí)行引擎進(jìn)行+1計(jì)算,計(jì)算后共享變量i=1。
- 在線程1的工作內(nèi)存中,通過assign指令將cpu執(zhí)行引擎計(jì)算后的共享變量i=1賦值到線程1的工作內(nèi)存中。
- 線程1通過store指令將線程1中工作內(nèi)存的共享變量同步到主內(nèi)存中。
- 在線程1的主內(nèi)存中,通過write指令將共享變量i=1的值賦值給主內(nèi)存的該共享變量,從而完成一次i++操作。
- 那么當(dāng)線程1還未將共享變量的值同步賦值回寫到主內(nèi)存時(shí),線程2開始進(jìn)行了i++操作,線程2通過read指令讀取到的共享變量i的值此時(shí)還是0,那么線程2又在0的基礎(chǔ)進(jìn)行了i++操作。所以當(dāng)很多線程并發(fā)執(zhí)行i++操作時(shí),結(jié)果是與我們預(yù)期不符的。
- 以上結(jié)合java內(nèi)存模型分析了我們共享變量的一個(gè)執(zhí)行流程。解釋了i++操作是一個(gè)線程不安全的。
結(jié)語(yǔ)
上一篇我們分析了java并發(fā)包中cas的原理,這篇總結(jié)下cas涉及到的java內(nèi)存模型的原理,cas還涉及到的cpu緩存一致性協(xié)議,我們后面繼續(xù)分析。
-
處理器
+關(guān)注
關(guān)注
68文章
19100瀏覽量
228814 -
寄存器
+關(guān)注
關(guān)注
31文章
5294瀏覽量
119814 -
JAVA
+關(guān)注
關(guān)注
19文章
2952瀏覽量
104477 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
904瀏覽量
28018
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論