Java內(nèi)存模型
在前面談到了一些關(guān)于內(nèi)存模型以及并發(fā)編程中可能會(huì)出現(xiàn)的一些問(wèn)題。下面我們來(lái)看一下Java內(nèi)存模型坟乾,研究一下Java內(nèi)存模型為我們提供了哪些保證以及在java中提供了哪些方法和機(jī)制來(lái)讓我們?cè)谶M(jìn)行多線程編程時(shí)能夠保證程序執(zhí)行的正確性迹辐。
在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來(lái)屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問(wèn)差異糊渊,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果右核。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問(wèn)規(guī)則渺绒,往大一點(diǎn)說(shuō)是定義了程序執(zhí)行的次序贺喝。注意,為了獲得較好的執(zhí)行性能宗兼,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來(lái)提升指令執(zhí)行速度躏鱼,也沒(méi)有限制編譯器對(duì)指令進(jìn)行重排序。也就是說(shuō)殷绍,在java內(nèi)存模型中染苛,也會(huì)存在緩存一致性問(wèn)題和指令重排序的問(wèn)題。
Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類似于前面說(shuō)的物理內(nèi)存)主到,每個(gè)線程都有自己的工作內(nèi)存(類似于前面的高速緩存)茶行。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主存進(jìn)行操作登钥。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存畔师。
舉個(gè)簡(jiǎn)單的例子:在java中,執(zhí)行下面這個(gè)語(yǔ)句:
1
i = 10;
執(zhí)行線程必須先在自己的工作線程中對(duì)變量i所在的緩存行進(jìn)行賦值操作牧牢,然后再寫入主存當(dāng)中看锉。而不是直接將數(shù)值10寫入主存當(dāng)中姿锭。
那么Java語(yǔ)言 本身對(duì) 原子性、可見(jiàn)性以及有序性提供了哪些保證呢伯铣?
1.原子性
在Java中呻此,對(duì)基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的腔寡,要么執(zhí)行焚鲜,要么不執(zhí)行。
上面一句話雖然看起來(lái)簡(jiǎn)單蹬蚁,但是理解起來(lái)并不是那么容易恃泪。看下面一個(gè)例子i:
請(qǐng)分析以下哪些操作是原子性操作:
x = 10; //語(yǔ)句1
y = x; //語(yǔ)句2
x++; //語(yǔ)句3
x = x + 1; //語(yǔ)句4
咋一看犀斋,有些朋友可能會(huì)說(shuō)上面的4個(gè)語(yǔ)句中的操作都是原子性操作贝乎。其實(shí)只有語(yǔ)句1是原子性操作,其他三個(gè)語(yǔ)句都不是原子性操作叽粹。
語(yǔ)句1是直接將數(shù)值10賦值給x览效,也就是說(shuō)線程執(zhí)行這個(gè)語(yǔ)句的會(huì)直接將數(shù)值10寫入到工作內(nèi)存中。
語(yǔ)句2實(shí)際上包含2個(gè)操作虫几,它先要去讀取x的值锤灿,再將x的值寫入工作內(nèi)存,雖然讀取x的值以及 將x的值寫入工作內(nèi)存 這2個(gè)操作都是原子性操作辆脸,但是合起來(lái)就不是原子性操作了但校。
同樣的,x++和 x = x+1包括3個(gè)操作:讀取x的值啡氢,進(jìn)行加1操作状囱,寫入新的值。
所以上面4個(gè)語(yǔ)句只有語(yǔ)句1的操作具備原子性倘是。
也就是說(shuō)亭枷,只有簡(jiǎn)單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量搀崭,變量之間的相互賦值不是原子操作)才是原子操作叨粘。
不過(guò)這里有一點(diǎn)需要注意:在32位平臺(tái)下,對(duì)64位數(shù)據(jù)的讀取和賦值是需要通過(guò)兩個(gè)操作來(lái)完成的瘤睹,不能保證其原子性升敲。但是好像在最新的JDK中,JVM已經(jīng)保證對(duì)64位數(shù)據(jù)的讀取和賦值也是原子性操作了轰传。
從上面可以看出驴党,Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作,如果要實(shí)現(xiàn)更大范圍操作的原子性绸吸,可以通過(guò)synchronized和Lock來(lái)實(shí)現(xiàn)鼻弧。由于synchronized和Lock能夠保證任一時(shí)刻只有一個(gè)線程執(zhí)行該代碼塊,那么自然就不存在原子性問(wèn)題了锦茁,從而保證了原子性攘轩。
2.可見(jiàn)性
對(duì)于可見(jiàn)性,Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性码俩。
當(dāng)一個(gè)共享變量被volatile修飾時(shí)度帮,它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí)稿存,它會(huì)去內(nèi)存中讀取新值笨篷。
而普通的共享變量不能保證可見(jiàn)性,因?yàn)槠胀ü蚕碜兞勘恍薷闹蟀曷模裁磿r(shí)候被寫入主存是不確定的率翅,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來(lái)的舊值袖迎,因此無(wú)法保證可見(jiàn)性冕臭。
另外,通過(guò)synchronized和Lock也能夠保證可見(jiàn)性燕锥,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼辜贵,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見(jiàn)性归形。
3.有序性
在Java內(nèi)存模型中托慨,允許編譯器和處理器對(duì)指令進(jìn)行重排序,但是重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行暇榴,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性厚棵。
在Java里面,可以通過(guò)volatile關(guān)鍵字來(lái)保證一定的“有序性”(具體原理在下一節(jié)講述)跺撼。另外可以通過(guò)synchronized和Lock來(lái)保證有序性窟感,很顯然,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼歉井,相當(dāng)于是讓線程順序執(zhí)行同步代碼柿祈,自然就保證了有序性。
另外哩至,Java內(nèi)存模型具備一些先天的“有序性”躏嚎,即不需要通過(guò)任何手段就能夠得到保證的有序性,這個(gè)通常也稱為 happens-before 原則菩貌。如果兩個(gè)操作的執(zhí)行次序無(wú)法從happens-before原則推導(dǎo)出來(lái)卢佣,那么它們就不能保證它們的有序性,虛擬機(jī)可以隨意地對(duì)它們進(jìn)行重排序箭阶。
下面就來(lái)具體介紹下happens-before原則(先行發(fā)生原則):
程序次序規(guī)則:一個(gè)線程內(nèi)虚茶,按照代碼順序戈鲁,書寫在前面的操作先行發(fā)生于書寫在后面的操作
鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作
volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C嘹叫,則可以得出操作A先行發(fā)生于操作C
線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作
線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè)婆殿,我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始
這8條原則摘自《深入理解Java虛擬機(jī)》罩扇。
這8條規(guī)則中婆芦,前4條規(guī)則是比較重要的,后4條規(guī)則都是顯而易見(jiàn)的喂饥。
下面我們來(lái)解釋一下前4條規(guī)則:
對(duì)于程序次序規(guī)則來(lái)說(shuō)消约,我的理解就是一段程序代碼的執(zhí)行在單個(gè)線程中看起來(lái)是有序的。注意员帮,雖然這條規(guī)則中提到“書寫在前面的操作先行發(fā)生于書寫在后面的操作”或粮,這個(gè)應(yīng)該是程序看起來(lái)執(zhí)行的順序是按照代碼順序執(zhí)行的,因?yàn)樘摂M機(jī)可能會(huì)對(duì)程序代碼進(jìn)行指令重排序集侯。雖然進(jìn)行重排序被啼,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致的,它只會(huì)對(duì)不存在數(shù)據(jù)依賴性的指令進(jìn)行重排序棠枉。因此浓体,在單個(gè)線程中,程序執(zhí)行看起來(lái)是有序執(zhí)行的辈讶,這一點(diǎn)要注意理解命浴。事實(shí)上,這個(gè)規(guī)則是用來(lái)保證程序在單線程中執(zhí)行結(jié)果的正確性贱除,但無(wú)法保證程序在多線程中執(zhí)行的正確性生闲。
第二條規(guī)則也比較容易理解,也就是說(shuō)無(wú)論在單線程中還是多線程中月幌,同一個(gè)鎖如果出于被鎖定的狀態(tài)碍讯,那么必須先對(duì)鎖進(jìn)行了釋放操作,后面才能繼續(xù)進(jìn)行l(wèi)ock操作扯躺。
第三條規(guī)則是一條比較重要的規(guī)則捉兴,也是后文將要重點(diǎn)講述的內(nèi)容。直觀地解釋就是录语,如果一個(gè)線程先去寫一個(gè)變量倍啥,然后一個(gè)線程去進(jìn)行讀取,那么寫入操作肯定會(huì)先行發(fā)生于讀操作澎埠。
第四條規(guī)則實(shí)際上就是體現(xiàn)happens-before原則具備傳遞性虽缕。