問:簡(jiǎn)單談?wù)勀銓?duì) Java 虛擬機(jī)內(nèi)存模型 JMM 的認(rèn)識(shí)和理解及并發(fā)中的原子性泳秀、可見性、有序性的理解榄攀?
答:Java 內(nèi)存模型主要目標(biāo)是定義程序中變量(此處變量特指實(shí)例字段嗜傅、靜態(tài)字段等,但不包括局部變量和函數(shù)參數(shù)檩赢,因?yàn)檫@兩種是線程私有無(wú)法共享)吕嘀。在虛擬機(jī)中存儲(chǔ)到內(nèi)存與從內(nèi)存讀取出來(lái)的規(guī)則細(xì)節(jié),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)存來(lái)完成(具體如下圖)乒融。
Java 內(nèi)存模型對(duì)主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議定義了八種操作掰盘,具體如下:
lock(鎖定):作用于主內(nèi)存變量摄悯,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
unlock(解鎖):作用于主內(nèi)存變量愧捕,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lá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í)執(zhí)行此操作轩缤。
assign(賦值):作用于工作內(nèi)存變量命迈,把一個(gè)從執(zhí)行引擎接收的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)需要給變量進(jìn)行賦值的字節(jié)碼指令時(shí)執(zhí)行此操作火的。
store(存儲(chǔ)):作用于工作內(nèi)存變量壶愤,把工作內(nèi)存中一個(gè)變量的值傳遞到主內(nèi)存中,以便后續(xù) write 操作馏鹤。
write(寫入):作用于主內(nèi)存變量征椒,把 store 操作從工作內(nèi)存中得到的值放入主內(nèi)存變量中。
如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存就必須按順序執(zhí)行 read 和 load 操作湃累,從工作內(nèi)存同步回主內(nèi)存就必須順序執(zhí)行 store 和 write 操作勃救,但是 JVM 只要求了操作的順序而沒有要求上述操作必須保證連續(xù)性,所以實(shí)質(zhì)執(zhí)行中 read 和 load 間及 store 和 write 間是可以插入其他指令的治力。
Java 內(nèi)存模型還會(huì)對(duì)指令進(jìn)行重排序操作蒙秒,在執(zhí)行程序時(shí)為了提高性能編譯器和處理器經(jīng)常會(huì)對(duì)指令進(jìn)行重排序操作,重排序主要分下面幾類:
編譯器優(yōu)化重排序:編譯器在不改變單線程程序語(yǔ)義的前提下可以重新安排語(yǔ)句的執(zhí)行順序宵统。
指令級(jí)并行重排序:現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行晕讲,如果不存在數(shù)據(jù)依賴性處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)重排序:由于處理器使用緩存和讀寫緩沖區(qū)使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行马澈。
其實(shí) Java JMM 內(nèi)存模型是圍繞并發(fā)編程中原子性瓢省、可見性、有序性三個(gè)特征來(lái)建立的箭券,關(guān)于原子性净捅、可見性、有序性的理解如下:
原子性:就是說一個(gè)操作不能被打斷辩块,要么執(zhí)行完要么不執(zhí)行蛔六,類似事務(wù)操作荆永,Java 基本類型數(shù)據(jù)的訪問大都是原子操作,long 和 double 類型是 64 位国章,在 32 位 JVM 中會(huì)將 64 位數(shù)據(jù)的讀寫操作分成兩次 32 位來(lái)處理具钥,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是說在并發(fā)訪問時(shí)是線程非安全的液兽,要想保證原子性就得對(duì)訪問該數(shù)據(jù)的地方進(jìn)行同步操作骂删,譬如 synchronized 等。
可見性:就是說當(dāng)一個(gè)線程對(duì)共享變量做了修改后其他線程可以立即感知到該共享變量的改變四啰,從 Java 內(nèi)存模型我們就能看出來(lái)多線程訪問共享變量都要經(jīng)過線程工作內(nèi)存到主存的復(fù)制和主存到線程工作內(nèi)存的復(fù)制操作宁玫,所以普通共享變量就無(wú)法保證可見性了;Java 提供了 volatile 修飾符來(lái)保證變量的可見性柑晒,每次使用 volatile 變量都會(huì)主動(dòng)從主存中刷新欧瘪,除此之外 synchronized、Lock匙赞、final 都可以保證變量的可見性佛掖。
有序性:就是說 Java 內(nèi)存模型中的指令重排不會(huì)影響單線程的執(zhí)行順序,但是會(huì)影響多線程并發(fā)執(zhí)行的正確性涌庭,所以在并發(fā)中我們必須要想辦法保證并發(fā)代碼的有序性芥被;在 Java 里可以通過 volatile 關(guān)鍵字保證一定的有序性,還可以通過 synchronized坐榆、Lock 來(lái)保證有序性拴魄,因?yàn)?synchronized、Lock 保證了每一時(shí)刻只有一個(gè)線程執(zhí)行同步代碼相當(dāng)于單線程執(zhí)行猛拴,所以自然不會(huì)有有序性的問題羹铅;除此之外 Java 內(nèi)存模型通過 happens-before 原則如果能推導(dǎo)出來(lái)兩個(gè)操作的執(zhí)行順序就能先天保證有序性,否則無(wú)法保證愉昆,關(guān)于 happens-before 原則可以查閱相關(guān)資料职员。
所以說如果想讓 Java 并發(fā)程序正確的執(zhí)行必須保證原子性、有序性跛溉、可見性焊切,只要三者中有任意一個(gè)不滿足并發(fā)都無(wú)法正確執(zhí)行。