本篇將介紹虛擬機(jī)如何實(shí)現(xiàn)多線程魂那、多線程之間由于共享和競(jìng)爭(zhēng)數(shù)據(jù)而導(dǎo)致的一系列問(wèn)題及解決方案烟瞧。
- 概述
- Java內(nèi)存模型
- Java與線程
1.概述
a.多任務(wù)處理的必要性:
- 充分利用計(jì)算機(jī)處理器的能力地粪,避免處理器在磁盤(pán)I/O、網(wǎng)絡(luò)通信或數(shù)據(jù)庫(kù)訪問(wèn)時(shí)總是處于等待其他資源的狀態(tài)新啼。
- 便于一個(gè)服務(wù)端同時(shí)對(duì)多個(gè)客戶(hù)端提供服務(wù)渣磷。通過(guò)指標(biāo)TPS(Transactions Per Second)可衡量一個(gè)服務(wù)性能的高低好壞,它表示每秒服務(wù)端平均能響應(yīng)的請(qǐng)求總數(shù)罢猪,進(jìn)而體現(xiàn)出程序的并發(fā)能力近她。
b.硬件的效率與一致性
為了更好的理解Java內(nèi)存模型,先理解物理計(jì)算機(jī)中的并發(fā)問(wèn)題膳帕,兩者有很高的可比性粘捎。
為了平衡計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度之間幾個(gè)數(shù)量級(jí)的差距,引入一層高速緩存(Cache)來(lái)作為內(nèi)存與處理器之間的緩沖:
- 將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中危彩,讓運(yùn)算能快速進(jìn)行攒磨;
- 當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,而無(wú)須讓處理器等待緩慢的內(nèi)存讀寫(xiě)汤徽。
但是基于高速緩存的存儲(chǔ)交互在多處理器系統(tǒng)中會(huì)帶來(lái)緩存一致性(Cache Coherence)的問(wèn)題娩缰。這是因?yàn)槊總€(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(Main Memory)谒府,當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí)拼坎,就可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。解決辦法就是需要各個(gè)處理器訪問(wèn)緩存時(shí)都遵循一些協(xié)議完疫,在讀寫(xiě)時(shí)要根據(jù)協(xié)議來(lái)進(jìn)行操作泰鸡。如下圖。
因此壳鹤,這里所說(shuō)的內(nèi)存模型可以理解為:在特定的操作協(xié)議下盛龄,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫(xiě)訪問(wèn)的過(guò)程抽象。
2.Java內(nèi)存模型(Java Memory Model芳誓,JMM)
a.目的:屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異余舶,實(shí)現(xiàn)Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
b.方法:通過(guò)定義程序中各個(gè)變量的訪問(wèn)規(guī)則锹淌,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)匿值。
注意:這里的變量與Java中說(shuō)的變量不同,而指的是實(shí)例字段葛圃、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素千扔,但不包括局部變量與方法參數(shù)憎妙,因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享曲楚,自然就不會(huì)存在競(jìng)爭(zhēng)問(wèn)題厘唾。
c.結(jié)構(gòu):模型結(jié)構(gòu)如圖,和上張圖進(jìn)行類(lèi)比龙誊。
- 主內(nèi)存(Main Memory):所有變量的存儲(chǔ)位置抚垃。直接對(duì)應(yīng)于物理硬件的內(nèi)存。
注意:這里的主內(nèi)存趟大、工作內(nèi)存與要點(diǎn)提煉| 理解JVM之內(nèi)存管理說(shuō)的Java內(nèi)存區(qū)域中的Java堆鹤树、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分逊朽。
- 工作內(nèi)存(Working Memory):每條線程還有自己的工作內(nèi)存罕伯,用于保存被該線程使用到的變量的主內(nèi)存副本拷貝。為了獲取更好的運(yùn)行速度叽讳,虛擬機(jī)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中追他。
注意:
- 線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量岛蚤。
- 不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量邑狸,線程間變量值的傳遞必須通過(guò)主內(nèi)存來(lái)完成。
-
交互協(xié)議:用于規(guī)定一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存涤妒、如何從工作內(nèi)存同步回主內(nèi)存之類(lèi)的實(shí)現(xiàn)細(xì)節(jié)单雾。共有8種操作:
- ①用于主內(nèi)存變量:
-
鎖定(
lock
):把變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。 -
解鎖(
unlock
):把處于鎖定狀態(tài)的變量釋放出來(lái)她紫。 -
讀取(
read
):把變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中硅堆,以便隨后的load
動(dòng)作使用。 -
載入(
load
):把read
操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中贿讹。 - ②用于工作內(nèi)存變量:
-
使用(
use
):把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎硬萍。 -
賦值(
assign
):把從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量。 -
存儲(chǔ)(
store
):把工作內(nèi)存中變量的值傳送到主內(nèi)存中围详,以便隨后的write
操作使用。 -
寫(xiě)入(
write
):把store
操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中祖屏。
結(jié)論:注意是順序非連續(xù)
- 如果要把變量從主內(nèi)存復(fù)制到工作內(nèi)存助赞,那就要順序地執(zhí)行
read
和load
。- 如果要把變量從工作內(nèi)存同步回主內(nèi)存袁勺,就要順序地執(zhí)行
store
和write
雹食。
d.確保并發(fā)操作安全的原則:
①在Java內(nèi)存模型中規(guī)定了執(zhí)行上述8種基本操作時(shí)需要滿(mǎn)足如下規(guī)則:
- 不允許
read
和load
、store
和write
操作之一單獨(dú)出現(xiàn)期丰,即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受群叶,或者從工作內(nèi)存發(fā)起回寫(xiě)了但主內(nèi)存不接受的情況出現(xiàn)吃挑。 - 不允許一個(gè)線程丟棄它的最近的
assign
操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存街立。 - 不允許一個(gè)線程無(wú)原因地舶衬,即沒(méi)有發(fā)生過(guò)任何
assign
操作就把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。 - 一個(gè)新的變量只能在主內(nèi)存中“誕生”赎离,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(
load
或assign
)的變量逛犹,即對(duì)一個(gè)變量實(shí)施use
、store
操作之前必須先執(zhí)行過(guò)了assign
和load
操作梁剔。 - 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行
lock
操作虽画,但lock
操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行lock
后荣病,只有執(zhí)行相同次數(shù)的unlock
操作码撰,變量才會(huì)被解鎖。 - 如果對(duì)一個(gè)變量執(zhí)行
lock
操作个盆,那將會(huì)清空工作內(nèi)存中此變量的值脖岛,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行load
或assign
操作初始化變量的值砾省。 - 如果一個(gè)變量事先沒(méi)有被
lock
操作鎖定鸡岗,那就不允許對(duì)它執(zhí)行unlock
操作,也不允許去unlock
一個(gè)被其他線程鎖定住的變量编兄。 - 對(duì)一個(gè)變量執(zhí)行
unlock
操作之前轩性,必須先把此變量同步回主內(nèi)存中。
可見(jiàn)這么多規(guī)則非常繁瑣狠鸳,實(shí)踐也麻煩揣苏,下面再介紹一個(gè)等效判斷原則--先行發(fā)生原則。
②先行發(fā)生原則:是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系件舵。下面例舉一些“天然的”先行發(fā)生關(guān)系卸察,無(wú)須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用铅祸。
- 程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi)坑质,按照控制流順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作临梗。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作涡扼。
- volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。
-
線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的
start()
先行發(fā)生于此線程的每一個(gè)動(dòng)作盟庞。 -
線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè).可通過(guò)
Thread.join()
結(jié)束吃沪、Thread.isAlive()
的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行。 -
線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程
interrupt()
的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生什猖∑北耄可通過(guò)Thread.interrupted()
檢測(cè)到是否有中斷發(fā)生红淡。 -
對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成先行發(fā)生于它的
finalize()
的開(kāi)始。 - 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B降铸,操作B先行發(fā)生于操作C在旱,那么操作A一定先行發(fā)生于操作C。
e.Java內(nèi)存模型保證并發(fā)過(guò)程的原子性垮耳、可見(jiàn)性和有序性的措施:
-
原子性(Atomicity):一個(gè)操作要么都執(zhí)行要么都不執(zhí)行颈渊。
- 可直接保證的原子性變量操作有:
read
、load
终佛、assign
俊嗽、use
、store
和write
铃彰,因此可認(rèn)為基本數(shù)據(jù)類(lèi)型的訪問(wèn)讀寫(xiě)是具備原子性的绍豁。 - 若需要保證更大范圍的原子性,可通過(guò)更高層次的字節(jié)碼指令
monitorenter
和monitorexit
來(lái)隱式地使用lock
和unlock
這兩個(gè)操作牙捉,反映到Java代碼中就是同步代碼塊synchronized
關(guān)鍵字竹揍。
- 可直接保證的原子性變量操作有:
-
可見(jiàn)性(Visibility):當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改邪铲。
- 通過(guò)在變量修改后將新值同步回主內(nèi)存芬位,在變量讀取前從主內(nèi)存刷新變量值這種依賴(lài)主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)。
- 提供三個(gè)關(guān)鍵字保證可見(jiàn)性:
volatile
能保證新值能立即同步到主內(nèi)存带到,且每次使用前立即從主內(nèi)存刷新昧碉;synchronized
對(duì)一個(gè)變量執(zhí)行unlock操作之前可以先把此變量同步回主內(nèi)存中;被final
修飾的字段在構(gòu)造器中一旦初始化完成且構(gòu)造器沒(méi)有把this
的引用傳遞出去揽惹,就可以在其他線程中就能看見(jiàn)final字段的值被饿。
-
有序性(Ordering):程序代碼按照指令順序執(zhí)行。
- 如果在本線程內(nèi)觀察搪搏,所有的操作都是有序的狭握,指“線程內(nèi)表現(xiàn)為串行的語(yǔ)義”;如果在一個(gè)線程中觀察另一個(gè)線程疯溺,所有的操作都是無(wú)序的论颅,指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。
- 提供兩個(gè)關(guān)鍵字保證有序性:
volatile
本身就包含了禁止指令重排序的語(yǔ)義囱嫩;synchronized
保證一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作嗅辣,使得持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。
3.Java與線程
a.線程實(shí)現(xiàn)的三種方式
①使用內(nèi)核線程(Kernel-Level Thread,KLT)
- 定義:直接由操作系統(tǒng)內(nèi)核支持的線程挠说。
- 原理:由內(nèi)核來(lái)完成線程切換,內(nèi)核通過(guò)操縱調(diào)度器(Scheduler)對(duì)線程進(jìn)行調(diào)度愿题,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上损俭。每個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身蛙奖, 這樣操作系統(tǒng)就有能力同時(shí)處理多件事情。
- 多線程內(nèi)核(Multi-Threads Kernel):支持多線程的內(nèi)核
-
輕量級(jí)進(jìn)程(Light Weight Process,LWP):內(nèi)核線程的一種高級(jí)接口
- 優(yōu)點(diǎn):每個(gè)輕量級(jí)進(jìn)程都由一個(gè)內(nèi)核線程支持杆兵,因此每個(gè)都成為一個(gè)獨(dú)立的調(diào)度單元雁仲,即使有一個(gè)輕量級(jí)進(jìn)程在系統(tǒng)調(diào)用中阻塞,也不會(huì)影響整個(gè)進(jìn)程繼續(xù)工作琐脏。
- 缺點(diǎn):由于基于內(nèi)核線程實(shí)現(xiàn)攒砖,所以各種線程操作(創(chuàng)建、析構(gòu)及同步)都需要進(jìn)行系統(tǒng)調(diào)用日裙,代價(jià)相對(duì)較高吹艇,需要在用戶(hù)態(tài)(User Mode)和內(nèi)核態(tài)(Kernel Mode)中來(lái)回切換;另外昂拂,一個(gè)系統(tǒng)支持輕量級(jí)進(jìn)程的數(shù)量是有限的受神。
- 一對(duì)一線程模型:輕量級(jí)進(jìn)程與內(nèi)核線程之間1:1的關(guān)系,如圖所示
②使用用戶(hù)線程(User Thread,UT)
- 定義:廣義上認(rèn)為一個(gè)線程不是內(nèi)核線程就是用戶(hù)線程格侯;狹義上認(rèn)為用戶(hù)線程指的是完全建立在用戶(hù)空間的線程庫(kù)上鼻听,而系統(tǒng)內(nèi)核不能感知線程存在的實(shí)現(xiàn)。
- 優(yōu)點(diǎn):由于用戶(hù)線程的建立联四、同步撑碴、銷(xiāo)毀和調(diào)度完全在用戶(hù)態(tài)中完成,不需要內(nèi)核的幫助朝墩,甚至可以不需要切換到內(nèi)核態(tài)醉拓,所以操作非常快速且低消耗的鱼辙,且可以支持規(guī)模更大的線程數(shù)量廉嚼。
- 缺點(diǎn):由于沒(méi)有系統(tǒng)內(nèi)核的支援,所有的線程操作都需要用戶(hù)程序自己處理倒戏,線程的創(chuàng)建怠噪、切換和調(diào)度都是需要考慮的問(wèn)題,實(shí)現(xiàn)較復(fù)雜杜跷。
- 一對(duì)多的線程模型進(jìn)程:進(jìn)程與用戶(hù)線程之間1:N的關(guān)系傍念,如圖所示
③使用用戶(hù)線程加輕量級(jí)進(jìn)程混合
- 定義:既存在用戶(hù)線程,也存在輕量級(jí)進(jìn)程葛闷。
- 優(yōu)點(diǎn):用戶(hù)線程完全建立在用戶(hù)空間中憋槐,因此用戶(hù)線程的創(chuàng)建、切換淑趾、析構(gòu)等操作依然廉價(jià)阳仔,并且可以支持大規(guī)模的用戶(hù)線程并發(fā);操作系統(tǒng)提供支持的輕量級(jí)進(jìn)程作為用戶(hù)線程和內(nèi)核線程之間的橋梁扣泊,可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射近范,且用戶(hù)線程的系統(tǒng)調(diào)用要通過(guò)輕量級(jí)線程來(lái)完成嘶摊,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn)。
- 多對(duì)多的線程模型:用戶(hù)線程與輕量級(jí)進(jìn)程的數(shù)量比不定评矩,即用戶(hù)線程與輕量級(jí)進(jìn)程之間N:M的關(guān)系叶堆,如圖所示
那么Java線程的實(shí)現(xiàn)是選擇哪一種呢?答案是不確定的斥杜。操作系統(tǒng)支持怎樣的線程模型虱颗,在很大程度上決定了Java虛擬機(jī)的線程是怎樣映射的。線程模型只對(duì)線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響蔗喂,而對(duì)Java程序的編碼和運(yùn)行過(guò)程來(lái)說(shuō)忘渔,這些差異都是透明的。
b.Java線程調(diào)度的兩種方式
線程調(diào)度:指系統(tǒng)為線程分配處理器使用權(quán)的過(guò)程弱恒。
①協(xié)同式線程調(diào)度(Cooperative Threads-Scheduling)
- 由線程本身來(lái)控制線程的執(zhí)行時(shí)間辨萍。線程把自己的工作執(zhí)行完后,要主動(dòng)通知系統(tǒng)切換到另外一個(gè)線程上返弹。
- 好處:實(shí)現(xiàn)簡(jiǎn)單锈玉;切換操作自己可知,不存在線程同步的問(wèn)題义起。
- 壞處:線程執(zhí)行時(shí)間不可控拉背,假如一個(gè)線程編寫(xiě)有問(wèn)題一直不告知系統(tǒng)進(jìn)行線程切換,那么程序就會(huì)一直被阻塞默终。
②搶占式線程調(diào)度(Preemptive Threads-Scheduling)
- 由系統(tǒng)來(lái)分配每個(gè)線程的執(zhí)行時(shí)間椅棺。
- 好處:線程執(zhí)行時(shí)間是系統(tǒng)可控的,不存在一個(gè)線程導(dǎo)致整個(gè)進(jìn)程阻塞的問(wèn)題齐蔽。
- 可以通過(guò)設(shè)置線程優(yōu)先級(jí)两疚,優(yōu)先級(jí)越高的線程越容易被系統(tǒng)選擇執(zhí)行。
但是線程優(yōu)先級(jí)并不是太靠譜含滴,一方面因?yàn)镴ava的線程是通過(guò)映射到系統(tǒng)的原生線程上來(lái)實(shí)現(xiàn)的诱渤,所以線程調(diào)度最終還是取決于操作系統(tǒng),在一些平臺(tái)上不同的優(yōu)先級(jí)實(shí)際會(huì)變得相同谈况;另一方面優(yōu)先級(jí)可能會(huì)被系統(tǒng)自行改變勺美。
c.線程的五種狀態(tài)
在任意一個(gè)時(shí)間點(diǎn),一個(gè)線程只能有且只有其中的一種狀態(tài):
- 新建(New):線程創(chuàng)建后尚未啟動(dòng)
- 運(yùn)行(Runable):包括正在執(zhí)行(Running)和等待著CPU為它分配執(zhí)行時(shí)間(Ready)兩種
-
無(wú)限期等待(Waiting):該線程不會(huì)被分配CPU執(zhí)行時(shí)間碑韵,要等待被其他線程顯式地喚醒赡茸。以下方法會(huì)讓線程陷入無(wú)限期等待狀態(tài):
- 沒(méi)有設(shè)置Timeout參數(shù)的
Object.wait()
- 沒(méi)有設(shè)置Timeout參數(shù)的
Thread.join()
LockSupport.park()
- 沒(méi)有設(shè)置Timeout參數(shù)的
-
限期等待(Timed Waiting):該線程不會(huì)被分配CPU執(zhí)行時(shí)間,但在一定時(shí)間后會(huì)被系統(tǒng)自動(dòng)喚醒祝闻。以下方法會(huì)讓線程進(jìn)入限期等待狀態(tài):
Thread.sleep()
- 設(shè)置了Timeout參數(shù)的
Object.wai()
- 設(shè)置了Timeout參數(shù)的
Thread.join()
LockSupport.parkNanos()
LockSupport.parkUntil()
- 阻塞(Blocked):線程被阻塞
注意區(qū)別:
- 阻塞狀態(tài):在等待獲取到一個(gè)排他鎖占卧,在另外一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;
- 等待狀態(tài):在等待一段時(shí)間或者喚醒動(dòng)作的發(fā)生,在程序等待進(jìn)入同步區(qū)域的時(shí)候發(fā)生屉栓。
- 結(jié)束(Terminated):線程已經(jīng)結(jié)束執(zhí)行
下圖是線程狀態(tài)之間的轉(zhuǎn)換: