在前面談到了一些關(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)行多線(xiàn)程編程時(shí)能夠保證程序執(zhí)行的正確性。
在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來(lái)屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪(fǎng)問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪(fǎng)問(wèn)效果。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪(fǎng)問(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)中(類(lèi)似于前面說(shuō)的物理內(nèi)存),每個(gè)線(xiàn)程都有自己的工作內(nèi)存(類(lèi)似于前面的高速緩存)。線(xiàn)程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主存進(jìn)行操作。并且每個(gè)線(xiàn)程不能訪(fǎng)問(wèn)其他線(xiàn)程的工作內(nèi)存。
舉個(gè)簡(jiǎn)單的例子:在java中,執(zhí)行下面這個(gè)語(yǔ)句:
i = 10;
執(zhí)行線(xiàn)程必須先在自己的工作線(xiàn)程中對(duì)變量i所在的緩存行進(jìn)行賦值操作,然后再寫(xiě)入主存當(dāng)中。而不是直接將數(shù)值10寫(xiě)入主存當(dāng)中。
那么Java語(yǔ)言 本身對(duì) 原子性、可見(jiàn)性以及有序性提供了哪些保證呢?
在Java中,對(duì)基本數(shù)據(jù)類(lèi)型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。
上面一句話(huà)雖然看起來(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ō)線(xiàn)程執(zhí)行這個(gè)語(yǔ)句的會(huì)直接將數(shù)值10寫(xiě)入到工作內(nèi)存中。
語(yǔ)句2實(shí)際上包含2個(gè)操作,它先要去讀取x的值,再將x的值寫(xiě)入工作內(nèi)存,雖然讀取x的值以及 將x的值寫(xiě)入工作內(nèi)存 這2個(gè)操作都是原子性操作,但是合起來(lái)就不是原子性操作了。
同樣的,x++和 x = x+1包括3個(gè)操作:讀取x的值,進(jìn)行加1操作,寫(xiě)入新的值。
所以上面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è)線(xiàn)程執(zhí)行該代碼塊,那么自然就不存在原子性問(wèn)題了,從而保證了原子性。
對(duì)于可見(jiàn)性,Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。
當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線(xiàn)程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見(jiàn)性,因?yàn)槠胀ü蚕碜兞勘恍薷闹?,什么時(shí)候被寫(xiě)入主存是不確定的,當(dāng)其他線(xiàn)程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來(lái)的舊值,因此無(wú)法保證可見(jiàn)性。
另外,通過(guò)synchronized和Lock也能夠保證可見(jiàn)性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線(xiàn)程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見(jiàn)性。
在Java內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,但是重排序過(guò)程不會(huì)影響到單線(xiàn)程程序的執(zhí)行,卻會(huì)影響到多線(xiàn)程并發(fā)執(zhí)行的正確性。
在Java里面,可以通過(guò)volatile關(guān)鍵字來(lái)保證一定的“有序性”(具體原理在下一節(jié)講述)。另外可以通過(guò)synchronized和Lock來(lái)保證有序性,很顯然,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線(xiàn)程執(zhí)行同步代碼,相當(dāng)于是讓線(xiàn)程順序執(zhí)行同步代碼,自然就保證了有序性。
另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過(guò)任何手段就能夠得到保證的有序性,這個(gè)通常也稱(chēng)為 happens-before 原則。如果兩個(gè)操作的執(zhí)行次序無(wú)法從happens-before原則推導(dǎo)出來(lái),那么它們就不能保證它們的有序性,虛擬機(jī)可以隨意地對(duì)它們進(jìn)行重排序。
下面就來(lái)具體介紹下happens-before原則(先行發(fā)生原則):
程序次序規(guī)則:一個(gè)線(xiàn)程內(nèi),按照代碼順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作
鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作
volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
線(xiàn)程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線(xiàn)程的每個(gè)一個(gè)動(dòng)作
線(xiàn)程中斷規(guī)則:對(duì)線(xiàn)程interrupt()方法的調(diào)用先行發(fā)生于被中斷線(xiàn)程的代碼檢測(cè)到中斷事件的發(fā)生
線(xiàn)程終結(jié)規(guī)則:線(xiàn)程中所有的操作都先行發(fā)生于線(xiàn)程的終止檢測(cè),我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線(xiàn)程已經(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è)線(xiàn)程中看起來(lái)是有序的。注意,雖然這條規(guī)則中提到“書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作”,這個(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ù)依賴(lài)性的指令進(jìn)行重排序。因此,在單個(gè)線(xiàn)程中,程序執(zhí)行看起來(lái)是有序執(zhí)行的,這一點(diǎn)要注意理解。事實(shí)上,這個(gè)規(guī)則是用來(lái)保證程序在單線(xiàn)程中執(zhí)行結(jié)果的正確性,但無(wú)法保證程序在多線(xiàn)程中執(zhí)行的正確性。
第二條規(guī)則也比較容易理解,也就是說(shuō)無(wú)論在單線(xiàn)程中還是多線(xiàn)程中,同一個(gè)鎖如果出于被鎖定的狀態(tài),那么必須先對(duì)鎖進(jìn)行了釋放操作,后面才能繼續(xù)進(jìn)行l(wèi)ock操作。
第三條規(guī)則是一條比較重要的規(guī)則,也是后文將要重點(diǎn)講述的內(nèi)容。直觀地解釋就是,如果一個(gè)線(xiàn)程先去寫(xiě)一個(gè)變量,然后一個(gè)線(xiàn)程去進(jìn)行讀取,那么寫(xiě)入操作肯定會(huì)先行發(fā)生于讀操作。
第四條規(guī)則實(shí)際上就是體現(xiàn)happens-before原則具備傳遞性。