Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異馋艺,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果斩松。
一.Java內(nèi)存模型
1. 主內(nèi)存與工作內(nèi)存
Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則衬衬,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)绣檬。
此處的變量與Java編程中所說的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的(如果局部變量是一個(gè)reference引用類型龙助,它引用的對象在Java堆中可被各個(gè)線程共享,但是reference本身在Java棧的局部變量表中蛛芥,它是線程私用的)提鸟,不會(huì)被共享,自然就不存在競爭問題仅淑。
Java 內(nèi)存模型規(guī)定了所有的變量都存在主內(nèi)存中称勋。
每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝漓糙,線程對變量的所有操作(讀取铣缠、賦值等)都必須在工作內(nèi)存中進(jìn)行烘嘱,而不能直接讀寫主內(nèi)存中的變量昆禽。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量蝗蛙,線程間變量值得傳遞均需要通過主內(nèi)存來完成。
如果對應(yīng)Java內(nèi)存來說(硬要說的話)醉鳖,主內(nèi)存主要對應(yīng)于Java堆中的對象實(shí)例數(shù)據(jù)部分捡硅,而工作內(nèi)存則對應(yīng)于虛擬機(jī)棧中的部分區(qū)域。
2. 內(nèi)存間交互操作
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交換協(xié)議盗棵,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存壮韭、如何從工作內(nèi)存同步回主內(nèi)存之類的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了一下八種操作來完成纹因,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的喷屋、不可再分的。
①.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)存中得到的變量的值放入主內(nèi)存的變量中。
其中恨闪,read load倘感、store write必須按順序執(zhí)行,但不保證是連續(xù)執(zhí)行咙咽。這些定義相當(dāng)嚴(yán)謹(jǐn)?shù)质址爆嵗下辏瑢?shí)踐起來很麻煩,所以我們一個(gè)等效判斷原則-------先行發(fā)生原則,用來確定一個(gè)訪問在并發(fā)環(huán)境下是否安全蜡豹。
3. 對于 volatile 型變量的特殊規(guī)則
關(guān)鍵字 volatile 可以說是Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制麸粮,但是它并不容易完全被正確、完整地理解镜廉。
當(dāng)一個(gè)變量定義為volatile之后弄诲,它將具備兩種特性。
第一是保證此變量對所有線程的可見性娇唯,這里的“可見性”是指當(dāng)一條線程修改了這個(gè)變量的值齐遵,新值對于其他線程來說是可以立即得知的。而普通變量不能做到這一點(diǎn)塔插,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成梗摇,線程A修改了一個(gè)普通變量的值,然后向主內(nèi)存進(jìn)行回寫想许,另外一個(gè)線程B在線程A回寫完成了之后再從主內(nèi)存進(jìn)行讀取操作留美,新變量值才會(huì)對線程B可見。
由于Java里面的運(yùn)算并非原子操作伸刃,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的谎砾。
這段代碼發(fā)起了20個(gè)線程,每個(gè)線程對race變量進(jìn)行10000自增操作捧颅,如果這段代碼能夠正確并發(fā)的話景图,最后的輸出結(jié)果應(yīng)該是200000,但是運(yùn)行后我們會(huì)發(fā)現(xiàn)輸出的結(jié)果都不一樣碉哑,都是小于200000的數(shù)字挚币。
由于volatile變量只能保證可見性,在不符合一下兩條規(guī)則的運(yùn)算場景中扣典,我們?nèi)匀灰ㄟ^加鎖來保證原子性妆毕。
①.運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值贮尖。
②.變量不需要與其他的狀態(tài)變量共同參與不變約束
第二個(gè)特性是禁止指令重排序優(yōu)化笛粘,普通的變量僅僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致湿硝。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過程中無法感知到這點(diǎn)薪前,這也就是Java內(nèi)存模型中描述的所謂的“線程內(nèi)表現(xiàn)為串行的語義”。
4.原子性关斜、可見性與有序性
Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性示括、可見性和有序性這3個(gè)特征來建立的。
4.1 原子性
由Java內(nèi)存模型來直接保證的原子性變量操作包括read痢畜、load垛膝、assign鳍侣、use、store和write吼拥,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問續(xù)寫是具備原子性的拱她。同樣,synchronized塊之間的操作也是具備原子性的扔罪。
4.2 可見性
可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改桶雀。我們之前說的volatile的特殊規(guī)則就是保證了新增能立即同步到主內(nèi)存矿酵,以及每次使用前立即從主內(nèi)存刷新,因此可以說矗积,volatile保證了多線程操作時(shí)變量的可見性全肮,而普通變量則不能保證這一點(diǎn)。
除了volatile之外棘捣,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性辜腺,即 synchronized 和 final 。同步塊的可見性是由“對一個(gè)變量執(zhí)行unlock操作之前乍恐,必須先把此變量同步回主內(nèi)存中”這條獲得的评疗,而 final 關(guān)鍵字的可見性是指:被 final 修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有把 this 的引用傳遞出去茵烈,那么在其他線程中就能看見 final 字段的值百匆,它們無需同步就能被其他線程正確訪問。
4.3 有序性
Java 程序有個(gè)天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察呜投,所有的操作都是有序的加匈;如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無序的仑荐。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”雕拼,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。
Java語言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性粘招,volatile 關(guān)鍵字本身就包含了禁止指令重排序的語義啥寇,而 synchronized 則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入洒扎。
5. 先行發(fā)生原則
如果Java內(nèi)存模型中所有的有序性都僅僅靠 volatile 和 synchronized 來完成示姿,那么有一些操作將會(huì)變得很煩瑣,但是我們在編寫Java代碼時(shí)并沒有感覺到這一點(diǎn)逊笆,這是因?yàn)镴ava語言中有個(gè)“先行發(fā)生原則”栈戳。這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競爭难裆、線程是否安全的主要依據(jù)子檀。
先行發(fā)生是指 Java 內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系镊掖,如果說操作A先行發(fā)生于操作B,其實(shí)就是說在發(fā)生操作B之前褂痰,操作A產(chǎn)生的影響能被操作B觀察到亩进。
下面是Java內(nèi)存模型下一些“天然的”先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無需任何同步器協(xié)助就已經(jīng)存在缩歪。
①.程序次序規(guī)則:在一個(gè)線程內(nèi)归薛,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作匪蝙。
②.管理鎖定規(guī)則:一個(gè) unlock 操作先行發(fā)生于后面對同一個(gè)鎖的lock操作主籍。這里必須強(qiáng)調(diào)是同一個(gè)鎖,而“后面”是指時(shí)間上的先后順序逛球。
③.volatile變量規(guī)則:對一個(gè) volatile 變量的寫操作先行發(fā)生于后面對這個(gè)變量的讀操作千元。“后面”是指時(shí)間上的先后順序颤绕。
④.線程啟動(dòng)規(guī)則:Thread 對象的 start() 方法先行發(fā)生于此線程的每一個(gè)動(dòng)作幸海。
⑤.線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測。
⑥.線程中斷規(guī)則:對線程 interrupt() 方法的調(diào)優(yōu)先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生奥务。
⑦.對象終結(jié)規(guī)則:一個(gè)對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始物独。
⑧.傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C氯葬,那就可以得出結(jié)論操作A先生發(fā)生于操作C的結(jié)論议纯。
二.Java與線程
1.線程的實(shí)現(xiàn)(現(xiàn)在還沒有涉及到Java線程 而是統(tǒng)一的線程實(shí)現(xiàn))
線程是比進(jìn)程更輕量級(jí)的調(diào)度執(zhí)行單位,線程的引入溢谤,可以把一個(gè)進(jìn)程的資源分配和執(zhí)行調(diào)度分開瞻凤,各個(gè)線程可以共享進(jìn)程資源,又可以獨(dú)立調(diào)度(線程是CPU調(diào)度的基本單位)世杀。
主流的操作系統(tǒng)都提供了線程實(shí)現(xiàn)阀参,Java線程:每個(gè)已經(jīng)執(zhí)行 start() 且還未結(jié)束的 Java.lang.Thread 類的實(shí)例就代表了一個(gè)線程。
實(shí)現(xiàn)線程主要有3種方式:使用內(nèi)核線程實(shí)現(xiàn)瞻坝、使用用戶線程實(shí)現(xiàn)和使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)蛛壳。
1.1 使用內(nèi)核線程實(shí)現(xiàn)
內(nèi)核線程就是直接由操作系統(tǒng)內(nèi)核支持的線程,這種線程由內(nèi)核來完成線程切換所刀,內(nèi)核通過操縱調(diào)度器對線程進(jìn)行調(diào)度衙荐,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上。
程序一般不會(huì)直接去使用內(nèi)核線程浮创,而是去使用內(nèi)核線程的一種高級(jí)接口------輕量級(jí)進(jìn)程忧吟,也就是我們通常意義上的線程,由于每個(gè)輕量級(jí)進(jìn)程都由一個(gè)內(nèi)核線程支持斩披,因此只有先支持內(nèi)核線程溜族,才能有輕量級(jí)進(jìn)程讹俊。這種輕量級(jí)進(jìn)程與內(nèi)核線程之間1:1的關(guān)系成為一對一的線程模型。
由于內(nèi)核線程的支持煌抒,每個(gè)輕量級(jí)進(jìn)程都成為一個(gè)獨(dú)立的調(diào)度單元仍劈,即使有一個(gè)輕量級(jí)進(jìn)程在系統(tǒng)調(diào)用中阻塞了,也不會(huì)影響整個(gè)進(jìn)程繼續(xù)工作寡壮。但是輕量級(jí)進(jìn)程具有它的局限性:首先贩疙,由于是基于內(nèi)核線程是實(shí)現(xiàn)的,所以各種線程操作况既,如創(chuàng)建这溅、析構(gòu)及同步,都需要進(jìn)行系統(tǒng)調(diào)用坏挠。而系統(tǒng)調(diào)用的代價(jià)相對較高,需要在用戶態(tài)與內(nèi)核態(tài)中來回切換邪乍。其次降狠,每個(gè)輕量級(jí)進(jìn)程都需要有一個(gè)內(nèi)核線程的支持,所以要消耗一定的內(nèi)核資源庇楞。
1.2 使用用戶線程實(shí)現(xiàn)
從廣義上講榜配,一個(gè)線程只要不是內(nèi)核線程,就可以認(rèn)為是用戶線程吕晌。而狹義上的用戶線程指的是完全建立在用戶空間的線程庫上蛋褥,系統(tǒng)內(nèi)核不能感知線程存在的實(shí)現(xiàn)。
用戶線程的建立睛驳、同步烙心、銷毀和調(diào)度完全在用戶態(tài)中完成,不需要內(nèi)核的幫助乏沸。這種進(jìn)程與用戶線程之間1:N的關(guān)系稱為一對多的線程模型淫茵。
使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于不需要系統(tǒng)內(nèi)核支援蹬跃,所有的線程操作都需要用戶程序自己處理匙瘪,很多問題解決起來會(huì)異常困難,所以現(xiàn)在很多語言都放棄了蝶缀。
1.3 使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)
這是一種將內(nèi)核線程與用戶線程一起使用的實(shí)現(xiàn)方式丹喻,在這種混合實(shí)現(xiàn)下,既存在用戶線程翁都,也存在輕量級(jí)進(jìn)程碍论。
操作系統(tǒng)提供支持的輕量級(jí)進(jìn)程作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射柄慰,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級(jí)線程來完成骑冗,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn)赊瞬。在這種混合模式中,用戶線程與輕量級(jí)進(jìn)程的數(shù)量比是不定的贼涩,即為N:M巧涧。
1.4 Java線程的實(shí)現(xiàn)
在目前的JDK版本中,操作系統(tǒng)支持怎樣的線程模型遥倦,在很大程度上決定了Java虛擬機(jī)的線程是怎樣映射的谤绳。線程模型只對線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響,對Java程序的編碼和運(yùn)行來說袒哥,這些差異都是透明的缩筛。
2. Java線程調(diào)度
線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程,主要調(diào)度方式有兩種堡称,分別是協(xié)同式調(diào)度和搶占式調(diào)度瞎抛。
2.1 協(xié)同式調(diào)度
如果使用協(xié)同式調(diào)度的多線程系統(tǒng),線程的執(zhí)行時(shí)間由線程本身控制却紧,線程把自己的工作執(zhí)行完了之后桐臊,就主動(dòng)通知系統(tǒng)切換到另一個(gè)線程上。
協(xié)同式調(diào)度的最大好處是實(shí)現(xiàn)簡單晓殊,而且由于線程要把自己的事情干完后才會(huì)進(jìn)行線程切換断凶,切換操作對線程自己是可知的,所以沒有什么線程同步的問題巫俺。
同時(shí)认烁,它的壞處是:線程執(zhí)行時(shí)間不可控制,甚至如果一個(gè)線程編寫有問題介汹,一直不告訴系統(tǒng)進(jìn)行線程切換却嗡,那么程序就會(huì)一直阻塞在那里。
2.2 搶占式調(diào)度
如果使用搶占式調(diào)度的多線程系統(tǒng)嘹承,那么每個(gè)線程將由系統(tǒng)來分配執(zhí)行時(shí)間稽穆,線程的切換不由線程本身來決定(在Java中,Thread.yield() 可以讓出執(zhí)行時(shí)間)赶撰。在這種實(shí)現(xiàn)線程調(diào)度的方式下舌镶,線程的執(zhí)行時(shí)間是系統(tǒng)可控的,也不會(huì)有一個(gè)線程導(dǎo)致整個(gè)進(jìn)程阻塞的問題豪娜,Java使用的線程調(diào)度方式就是搶占式調(diào)度餐胀。例如 Windows ,當(dāng)一個(gè)進(jìn)程出現(xiàn)問題瘤载,我們還可以使用任務(wù)管理器把 這個(gè)進(jìn)程“殺掉”否灾。
雖然Java線程的調(diào)度室系統(tǒng)自動(dòng)完成的,但是我們還可以“建議”系統(tǒng)給某些線程多分配一點(diǎn)執(zhí)行時(shí)間鸣奔,另外的一下線程則可以少分配一點(diǎn)-----這項(xiàng)操作可以通過設(shè)置線程優(yōu)先級(jí)來完成墨技。
在兩個(gè)線程同時(shí)處于Ready狀態(tài)時(shí)惩阶,優(yōu)先級(jí)越高的線程越容易被系統(tǒng)選擇執(zhí)行。
不過線程游戲家也并不是太靠譜扣汪,原因是Java的線程是通過映射到系統(tǒng)的原生線程上來實(shí)現(xiàn)的断楷,所以線程調(diào)度最終還是取決于操作系統(tǒng),在Windows系統(tǒng)存在一個(gè)成為“優(yōu)先級(jí)推進(jìn)器”的功能崭别,它的大致作用就是當(dāng)系統(tǒng)發(fā)現(xiàn)一個(gè)線程執(zhí)行得特別“勤奮努力”的話冬筒,可能會(huì)越過線程優(yōu)先級(jí)去為它分配執(zhí)行時(shí)間。
3. 狀態(tài)交換
Java語言定義了5種線程狀態(tài)茅主,在任意個(gè)時(shí)間點(diǎn)舞痰,一個(gè)線程只能有且只有其中的一種狀態(tài)。
1.新建(New):創(chuàng)建后尚未啟動(dòng)的線程處于這種狀態(tài)诀姚。
2.運(yùn)行(Runable): 包括了操作系統(tǒng)線程狀態(tài)中的 Running 和 Ready响牛,也就是處于次狀態(tài)的線程有可能正在執(zhí)行,也有可能正在等待著 CPU 為它分配執(zhí)行時(shí)間赫段。
3.無限期等待 (Waiting):線程不會(huì)被分配CPU執(zhí)行時(shí)間呀打,它們要等待被其他線程顯示地喚醒,以下方法會(huì)讓線程陷入無限期的等待狀態(tài)瑞佩。
①.沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法聚磺。
②.沒有設(shè)置 Timeout 參數(shù)的 Thread.join() 方法坯台。
③.LockSupport.park() 方法
4.有限期等待(Timed Waiting): 線程也不會(huì)被分配 CPU 執(zhí)行時(shí)間炬丸,不過無需等待被其他線程顯示的喚醒,在一定時(shí)間之后它們會(huì)由系統(tǒng)自動(dòng)喚醒蜒蕾。以下方法會(huì)讓線程陷入有限期的等待狀態(tài)稠炬。
①.Thread.sleep() 方法
②.設(shè)置 Timeout 參數(shù)的 Object.wait() 方法。.
③.設(shè)置 Timeout 參數(shù)的 Thread.join() 方法咪啡。
④.LockSupport.parkNanos()?
⑤.LockSupport.parkUnitl()
5.阻塞(Blocked):線程被阻塞了首启,“阻塞狀態(tài)”與“等待狀態(tài)”的區(qū)別是:阻塞狀態(tài)在等待著獲取一個(gè)排它鎖,這個(gè)時(shí)間將在另外一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生撤摸;而“等待狀態(tài)”則是在等待一段時(shí)間毅桃,或者喚醒動(dòng)作的發(fā)生,在程序等待進(jìn)入同步區(qū)域的時(shí)候准夷,線程將進(jìn)入這種狀態(tài)钥飞。
6.結(jié)束:已終止的線程狀態(tài)。
摘自《深入理解Java虛擬機(jī)》