1. 緩存一致性
計算機并發(fā)執(zhí)行若干任務号涯,需要與內(nèi)存交互。計算機存儲設備處理速度與處理器處理速度有著量級的差距芜抒,所以現(xiàn)代計算機系統(tǒng)不得不加入一層讀寫速度盡可能接近處理器運算速度的“高速緩存”來處理內(nèi)存與處理器之間的緩沖:將運算需要使用的數(shù)據(jù)復制到緩存中徽诲,讓運算快速進行劝萤,運算結束后再從緩存同步到內(nèi)存中,這樣處理器無需等待緩慢的內(nèi)存讀寫了煞赢。
但是這樣引入了一個問題:緩存一致性抛计。多處理器系統(tǒng),每個系統(tǒng)有自己的高速緩存照筑,而又共享同一主內(nèi)存吹截。當多個處理器運算任務都涉及同一主內(nèi)存區(qū)域時瘦陈,將導致各自的緩存數(shù)據(jù)不一致,如果真的發(fā)生這種情況波俄,同步回主內(nèi)存時以誰的緩存數(shù)據(jù)為準呢晨逝?為了解決一致性的問題,需要各個處理器訪問緩存時遵循一些協(xié)議懦铺,在讀寫時根據(jù)協(xié)議進行操作捉貌,如MESI。
MESI緩存一致性協(xié)議
如果變量X是一個如valotile修飾的變量冬念,相當于在底層加了一個#Lock趁窃,此時會觸發(fā)MESI緩存一致性協(xié)議。
1.T1線程讀取了主內(nèi)存變量X刘急,將變量X標記為E狀態(tài)棚菊,同時會一直監(jiān)聽/嗅探bus其他線程是否有對變量X的操作。
2.如果此時線程T2也讀取了變量X叔汁,此時嗅探機制起作用统求,線程1與線程2會將變量X都編輯為S狀態(tài)。
3.如果線程T1修改了變量X据块,先鎖住該變量緩存行+將變量X標記為M+向bus發(fā)送消息码邻,同時線程T2會嗅探到bus消息中其他線程要修改變量X,這時線程T2會將變量X標記為I另假。需要重新讀取主存的變量X像屋。
4.線程T1修改完變量X后,將狀態(tài)標記為E边篮,并寫回主內(nèi)存中X新值己莺。
狀態(tài) | 描述 | 監(jiān)聽任務 |
---|---|---|
M 修改 (Modified) | 該Cache line有效,數(shù)據(jù)被修改了戈轿,和內(nèi)存中的數(shù)據(jù)不一致凌受,數(shù)據(jù)只存在于本Cache中。 | 緩存行必須時刻監(jiān)聽所有試圖讀該緩存行相對就主存的操作思杯,這種操作必須在緩存將該緩存行寫回主存并將狀態(tài)變成S(共享)狀態(tài)之前被延遲執(zhí)行胜蛉。 |
E 獨享、互斥 (Exclusive) | 該Cache line有效色乾,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致誊册,數(shù)據(jù)只存在于本Cache中。 | 緩存行也必須監(jiān)聽其它緩存讀主存中該緩存行的操作暖璧,一旦有這種操作案怯,該緩存行需要變成S(共享)狀態(tài)。 |
S 共享 (Shared) | 該Cache line有效漆撞,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致殴泰,數(shù)據(jù)存在于很多Cache中于宙。 | 緩存行也必須監(jiān)聽其它緩存使該緩存行無效或者獨享該緩存行的請求,并將該緩存行變成無效(Invalid)悍汛。 |
I 無效 (Invalid) | 該Cache line無效捞魁。 | 無 |
總線鎖與緩存一致性協(xié)議:如果是一個緩存行(緩存最小單位),則使用緩存一致性協(xié)議离咐,多行使用總線鎖谱俭。
2. java內(nèi)存模型(java memory model, JMM)
java虛擬機規(guī)范試圖定義一種java內(nèi)存模型來屏蔽掉各種硬件與操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)java程序在各種平臺下都能達到一致的內(nèi)存訪問效果宵蛀。
2.1 主內(nèi)存與工作內(nèi)存
java內(nèi)存模型主要目標是定義程序中各個變量的訪問規(guī)則昆著,其是一個抽象,即咋虛擬機中將變量存儲到內(nèi)存和從內(nèi)存取出變量這樣的底層細節(jié)术陶。此處的變量包括****實例對象凑懂,靜態(tài)字段和構成數(shù)組對象的元素,但不包括局部變量與方法參數(shù)梧宫。因為后者是線程私有的接谨,不會被共享,自然不會存在競爭問題塘匣。
java內(nèi)存模型規(guī)定了所有的變量存儲在主內(nèi)存中脓豪,對應硬件的主內(nèi)存。每條線程還有自己的工作內(nèi)存忌卤,對應硬件的CPU緩存扫夜、寄存器。線程對變量的所有操作(讀取驰徊,賦值等)必須在工作內(nèi)存中進行笤闯,而不能直接讀寫主內(nèi)存中的變量。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量棍厂,線程間變量值的傳遞均需要主內(nèi)存完成望侈。
java內(nèi)存模型與java內(nèi)存區(qū)域并不是一個層次的內(nèi)存劃分,基本上沒有聯(lián)系勋桶。如果非要對應的話,主內(nèi)存主要對應java堆中的對象實例數(shù)據(jù)部分侥猬,而工作內(nèi)存則對應虛擬機棧的部分區(qū)域例驹。
2.2 內(nèi)存見交互操作
關于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存退唠、如何從工作內(nèi)存同步回主內(nèi)存的實現(xiàn)細節(jié)鹃锈,java內(nèi)存模型定義了8種操作完成,以下每種操作都是原子的瞧预,不可再分的屎债。
double仅政,long特殊規(guī)則:虛允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫劃分為兩次32位的操作,即虛允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load\store\read\write操作的原子性盆驹,這就是所謂的double和long的非原子性協(xié)定圆丹。但是目前各種平臺的商用虛擬機幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作對待,因為編寫代碼時一般不需要把用到的long和double變量專門聲明為volatile躯喇。
(1)lock(鎖定):作用于主內(nèi)存的變量辫封,把一個變量標記為一條線程獨占狀態(tài)
(2)unlock(解鎖):作用于主內(nèi)存的變量,把一個處于鎖定狀態(tài)的變量釋放出來廉丽,釋放后的 變量才可以被其他線程鎖定
(3)read(讀取):作用于主內(nèi)存的變量倦微,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中, 以便隨后的load動作使用
(4)load(載入):作用于工作內(nèi)存的變量正压,它把read操作從主內(nèi)存中得到的變量值放入工作 內(nèi)存的變量副本中
(5)use(使用):作用于工作內(nèi)存的變量欣福,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要變量的值的字節(jié)碼指令時將會執(zhí)行這個操作
(6)assign(賦值):作用于工作內(nèi)存的變量焦履,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量拓劝,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時將會執(zhí)行這個操作
(7)store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中裁良, 以便隨后的write的操作
(8)write(寫入):作用于工作內(nèi)存的變量凿将,它把store操作從工作內(nèi)存中的一個變量的值傳送到主內(nèi)存的變量中
主內(nèi)存復制到工作內(nèi)存:read+load
工作內(nèi)存復制到主內(nèi)存:store+write
注:java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行价脾。即可能出現(xiàn)read a牧抵、read b、load b侨把、load a犀变。
規(guī)則:
- 不允許read和load、store和write操作單獨出現(xiàn)秋柄,即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受获枝,或相反的情況出現(xiàn)
- 不允許一個線程丟棄他的最近的assign操作,即變量在工作內(nèi)存改變了之后必須把該變化同步回主內(nèi)存
- 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
- 一個新的變量只能在主內(nèi)存中誕生骇笔,不允許在工作內(nèi)存中直接使用一個未被初始化(load或者assign)的變量省店。即就是對一個變量實施use和store操作之前,必須先自行assign和load操作笨触。
- 一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作懦傍,但lock操作可以被同一線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock后芦劣,只有執(zhí)行相同次數(shù)的unlock操作粗俱,變量才會被解鎖。lock和unlock必須成對出現(xiàn)虚吟。
- 如果對一個變量執(zhí)行l(wèi)ock操作寸认,將會清空工作內(nèi)存中此變量的值签财,在執(zhí)行引擎使用這個變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個變量事先沒有被lock操作鎖定偏塞,則不允許對它執(zhí)行unlock操作唱蒸;也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執(zhí)行unlock操作之前烛愧,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)
2.3 對于volatile型變量的特殊規(guī)則
vaolatile是java虛擬機提供的最輕量級的同步機制油宜。
特性1.保證此變量對所有線程的可見性。這里的可見性是指一條線程修改了這個變量的值怜姿,新值對于其他線程來說是可以立即得知的慎冤。普通變量的值在線程間傳遞均需要主內(nèi)存來完成。
(1)運算結果并不依賴于變量的當前值沧卢,或者能夠確保只有單一的線程修改變量的值
(2)變量不需要與其他的狀態(tài)變量共同參與不變約束蚁堤。
滿足以上兩點,volatile可保證原子性但狭。
特性2.禁止指令重排序優(yōu)化披诗。普通變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致(As-If-Serial)立磁。
指令重排
編譯器以及CPU為了優(yōu)化代碼或者執(zhí)行的效率而執(zhí)行的優(yōu)化操作
無論重排前還是重排后程序的執(zhí)行結果在單線程中并沒有改變呈队,因此這種重排優(yōu)化是允許的。
指令重排一般發(fā)生在編譯器和CPU運行時唱歧。
As-If-Serial:不管怎么重排序(編譯器和處理器為了提高并行度)宪摧,(單線程)程序的執(zhí)行結果不能被改變,多線程不遵守此原則颅崩。
內(nèi)存屏障
1.保證特定操作的執(zhí)行順序
2.保證某些變量的內(nèi)存可見性
通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化几于,禁止指令重排。
內(nèi)存屏障插入規(guī)則
是否能重排序 | 第二個操作 | ||
---|---|---|---|
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
JMM保守策略內(nèi)存屏障插入規(guī)則
在每個volatile寫操作的前面插入一個StoreStore屏障沿后。
在每個volatile寫操作的后面插入一個StoreLoad屏障沿彭。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadStore屏障尖滚。
2.4原子性喉刘、可見性、有序性
原子性:由java內(nèi)存模型直接保證的原子性變量操作包括read漆弄、load饱搏、assign、use置逻、store、write备绽,基本數(shù)據(jù)類型的訪問讀寫是具備原子性的券坞。大范圍的原子性保證鬓催,java內(nèi)存模型提供了lock、unlock來滿足恨锚,虛擬機提供了更高層次的字節(jié)碼指令monitorenter和monitorexit隱式對應宇驾,反映到java代碼就是同步塊-synchronized關鍵字。
可見性:一個線程修改了共享變量的值猴伶,其他線程能立即得知這個修改课舍。java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的他挎,無論是普通變量還是volatile變量都是如此筝尾,普通變量與volatile變量變量的區(qū)別是,volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存办桨,以及每次使用前立即從主內(nèi)存刷新筹淫。因此,可以說volatile保證了多線程操作時變量的可見性呢撞,而普通變量則不能保證這一點损姜。對應上邊MESI一致性協(xié)議分析
synchronized和final也能實現(xiàn)可見性。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前殊霞,必須先把此變量同步回主內(nèi)存中(執(zhí)行store摧阅、write操作)”這條規(guī)則獲得的。
有序性:如果在本線程內(nèi)觀察绷蹲,所有操作都是有序的(串行As-If-Serial)棒卷;如果在一個線程觀察另一個線程,所有操作都是無序的(指令重排瘸右,工作內(nèi)存與主內(nèi)存同步延遲)娇跟。volatile有序性本身就包含了禁止指令重排的語義;synchronized有序性是由“一個變量在同一時刻只允許一條進程對其進行l(wèi)ock操作”保證的太颤。
2.5先行發(fā)生原則
先行發(fā)生是java內(nèi)存模型定義的兩項操作之間的偏序關系苞俘,如果操作A先行發(fā)生于操作B,其實就是發(fā)生在操作B之前龄章,操作A產(chǎn)生的影響能被操作B觀察到吃谣,“影響”包括修改內(nèi)存中共享變量的值,發(fā)送了消息做裙,調(diào)用了方法等岗憋。
天然先行發(fā)生關系:
1.線程次序規(guī)則:在一個線程內(nèi),按照程序代碼順序锚贱,書寫在前面的操作先行發(fā)生于書寫在后面的操作仔戈。準確地說,應該是控制流順序而不是程序代碼順序,因為要考慮分支监徘、循環(huán)等結構
2.管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作晋修。這里必須強調(diào)的是同一個鎖,而“后面”是指時間上的先后順序凰盔。
3.volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作墓卦,這里的“后面”同樣是指時間上的先后順序。
4.線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作户敬。
5.線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測落剪,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段加測到線程已經(jīng)終止執(zhí)行尿庐。
6.線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生忠怖,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
7.對象終結規(guī)則:一個對象的初始化完成(構造函數(shù)執(zhí)行結束)先行發(fā)生于它的finalize()方法的開始屁倔。
8.傳遞性:如果操作A先行發(fā)生于操作B脑又,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結論锐借。
如果兩個操作之間的關系不在天然發(fā)生關系中问麸,就沒有順序保障,虛擬機可以對他們隨意的進行重排序钞翔。
3 java與線程
3.1 線程的實現(xiàn)
使用內(nèi)核線程實現(xiàn)严卖、使用用戶線程實現(xiàn)、使用用戶線程加輕量級進程混合實現(xiàn)
1.內(nèi)核線程實現(xiàn)
內(nèi)核線程(KTL)就是直接由操作系統(tǒng)內(nèi)核支持的線程布轿,這種線程由內(nèi)核完成線程切換哮笆,內(nèi)核通過操縱調(diào)度器對線程進行調(diào)度,并負責將線程的任務映射到各個處理器上
程序一般不會直接去使用內(nèi)核線程汰扭,而是去使用內(nèi)核線程的一種高級接口-輕量級進程(LWP)稠肘,輕量級進程就是我們通常意義上所講的進程,由于每個輕量級進程都由一個內(nèi)核線程支持萝毛,因此只有先支持內(nèi)核線程项阴,才能有輕量級進程。這種輕量級進程與內(nèi)核線程之間1:1的關系稱為一對一線程模型笆包。
輕量級進程局限性:
(1)基于內(nèi)核實現(xiàn)环揽,各種線程操作,如創(chuàng)建庵佣,析構及同步歉胶,都需要進行系統(tǒng)調(diào)用。而系統(tǒng)調(diào)用代價相對較高巴粪,需要在用戶態(tài)和內(nèi)核態(tài)來回切換
(2)每個輕量級進程都需要有一個內(nèi)核線程的支持通今,因此輕量級進程要消耗一定的內(nèi)核資源(如內(nèi)核線程的椫嗝空間),因此一個系統(tǒng)支持輕量級進程的數(shù)量時有限的衡创。
2.使用用戶線程實現(xiàn)
廣義講帝嗡,一個線程只要不是內(nèi)核線程,就可以認為是用戶線程(UT)璃氢。狹義上用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)狮辽。用戶線程的建立一也、同步、銷毀和調(diào)度完全在用戶態(tài)中完成喉脖,不需要內(nèi)核的幫助椰苟。部分高性能數(shù)據(jù)庫中的多線程是由用戶線程實現(xiàn)的。這種進程與用戶線程之間1:N的關系稱為一對多的線程模型树叽。
用戶線程優(yōu)勢:不需要系統(tǒng)內(nèi)核支持
用戶線程劣勢:沒有內(nèi)核的支持舆蝴,所有線程操作需要用戶程序自己處理。(1).要考慮線程創(chuàng)建题诵、切換洁仗、調(diào)度 (2).由于系統(tǒng)操作只把處理器資源分配到進程,哪諸如“阻塞處理”性锭,“多處理器系統(tǒng)如何將線程映射到其他處理器上”這些問題解決起來異常困難赠潦。
3.使用用戶線程加輕量級進程混合實現(xiàn)
用戶線程建立在用戶空間中,因此用戶線程的創(chuàng)建草冈、切換烤送、析構等操作依然廉價蟆豫,并且可以支持大規(guī)模的用戶線程并發(fā)。而操作系統(tǒng)提供支持的輕量級進程則作為用戶線程和內(nèi)核進程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射好渠,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險终畅。這種混合模式中渴肉,用戶線程與輕量級進程的數(shù)量比是不定的,即N:M诅岩,多對多的線程模型讳苦。
4.java線程的實現(xiàn)
jdk12以前,用戶線程吩谦。
jdk12以后鸳谜,線程模型替換為基于操作系統(tǒng)原生線程模型來實現(xiàn)。windows和linux使用一對一線程模型式廷,一條java線程就映射到一條輕量級進程中(因為linux和windows系統(tǒng)提供的線程模型就是一對一的)咐扭。solaris同時支持一對一和多對多,可通過參數(shù)配置。
3.2 java線程調(diào)度
線程調(diào)度是指系統(tǒng)為線程分配處理器使用權的過程蝗肪,分為協(xié)同式調(diào)度和搶占式調(diào)度袜爪。
協(xié)同式:現(xiàn)成的執(zhí)行時間由線程本身控制,線程把自己的工作處理完了之后薛闪,主動通知系統(tǒng)切換到另一個線程上辛馆。好處:實現(xiàn)簡單,切換操作自己可知豁延,沒有線程同步問題昙篙。壞處:線程執(zhí)行時間不可控制,如果一個線程編寫有問題诱咏,一直不告知系統(tǒng)進行線程抓換苔可,程序會阻塞住。
搶占式:每個線程將由系統(tǒng)分配執(zhí)行時間袋狞,線程的切換不由線程本身來決定(java Thread.yield可以讓出執(zhí)行時間焚辅,但是線程本身還是沒辦法獲取執(zhí)行時間)。好處:線程執(zhí)行時間系統(tǒng)可控苟鸯,不會出現(xiàn)一個線程阻塞程序的問題同蜻,java使用的就是這種。
雖然java線程調(diào)度是系統(tǒng)自動完成的倔毙,但是可以建議系統(tǒng)給某些線程多分配一點執(zhí)行時間埃仪,另外的一些少點執(zhí)行時間,即可通過設置線程優(yōu)先級來完成陕赃。java一共有10個優(yōu)先級卵蛉,多個和線程同時處于Ready狀態(tài)時,優(yōu)先級越高越容易被系統(tǒng)選擇執(zhí)行么库。
不過優(yōu)先級并不完全靠譜傻丝,因為基于的系統(tǒng)原生線程實現(xiàn),所以取決于操作系統(tǒng)诉儒。1.操作系統(tǒng)的優(yōu)先級與java的優(yōu)先級級別并不一定可以一一對應葡缰。2.系統(tǒng)可能會自動改變,如windows既有優(yōu)先級推進器忱反,會識別特別勤奮的線程泛释,越過優(yōu)先級為它分配執(zhí)行時間。
3.3 狀態(tài)轉(zhuǎn)換
在任意時間點温算,一個線程有且只能由一種狀態(tài)怜校。
- 新建(NEW):創(chuàng)建后尚未啟動的線程
- 運行(RUNNABLE):Runable包括了操作系統(tǒng)線程狀態(tài)的Running和Ready,也就是處于此狀態(tài)的線程有可能正在執(zhí)行注竿,也可能在等待CPU為他分配執(zhí)行時間茄茁。
- 無限期等待(WAITING):處于該狀態(tài)的線程不會被分配CPU執(zhí)行時間魂贬,他們要等待被其他線程顯式的喚醒,以下方法會陷入無限期等待狀態(tài):
沒有設置Timeout參數(shù)的Object.wait()方法
沒有設置Timeout參數(shù)的Thread.join()方法
LockSupport.park()方法
- 限期等待(TIMED WAITING):處于該狀態(tài)的線程也不會被分配CPU執(zhí)行時間裙顽,不過無需等待被其他線程顯式的喚醒付燥,在一定時間后會由系統(tǒng)自動喚醒,以下方法會陷入限期等待狀態(tài):
Thread.sleep()方法 設置Timeout參數(shù)的Object.wait()方法
設置Timeout參數(shù)的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法
- 阻塞(BLOCKED):線程被阻塞了愈犹,阻塞狀態(tài)與等待狀態(tài)區(qū)別是:阻塞狀態(tài)在等待著獲取到一個排他鎖键科,這個事件將在另外一個線程放棄這個鎖的時候發(fā)生;而等待狀態(tài)則是在等待一段時間漩怎,或者喚醒動作的發(fā)生萝嘁。在程序等待進入同步區(qū)域的時候,線程進入這種狀態(tài)扬卷。
- 結束(TERMINATED):線程已經(jīng)執(zhí)行完畢。