- Java內(nèi)存模型基礎(chǔ)知識
- 重排序與happens-before
- volatile
- synchronized與鎖
- CAS與原子操作
- AQS
6.Java內(nèi)存模型基礎(chǔ)知識
6.1 并發(fā)編程模型的兩個關(guān)鍵問題
- 通信:交換信息的機制
- 同步:控制不同線程間操作發(fā)生的相對順序
并發(fā)模型
- 消息傳遞并發(fā)模型
- 共享內(nèi)存并發(fā)模型
兩種模型之間區(qū)別:
在Java中蝎宇,使用的是共享內(nèi)存并發(fā)模型
6.2 java內(nèi)存模型的抽象結(jié)構(gòu)
6.2.1 運行時內(nèi)存的劃分
每個線程的棧是私有的膝舅,堆是公有的详囤。棧中的局部變量,方法定義參數(shù)锄贼,異常處理參數(shù)不會在線程之間共享,也不存在內(nèi)存可見性雷猪,不會受內(nèi)存模型的影響票从。
內(nèi)存可見性針對的是共享變量。
6.2.2 既然堆是共享的罩抗,為什么在堆中會有內(nèi)存不可見問題拉庵?
因為現(xiàn)代計算機為了高效,往往會在高速緩存區(qū)中緩存共享變量套蒂,因為cpu訪問緩存區(qū)比訪問內(nèi)存要快得多。
線程之間的共享內(nèi)存存在主內(nèi)存中,每個線程都有一個私有的本地內(nèi)存憔恳,存儲了該線程以讀吩翻、寫共享變量的副本。本地內(nèi)存是Java內(nèi)存模型的一個抽象概念骨坑,并不真實存在撼嗓。它涵蓋了緩存、寫緩沖區(qū)欢唾、寄存器等(用于提升CPU的訪問速度)且警。
Java線程之間的通信由Java內(nèi)存模型(簡稱JMM)控制,從抽象的角度來說礁遣,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系斑芜。
由圖可知:
- 所有的共享變量都存在主內(nèi)存中。
- 每個線程都保存了一份該線程使用到的共享變量的副本祟霍。
- 如果線程A與線程B之間要通信的話押搪,必須經(jīng)歷下面2個步驟:
1)線程A將本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
2)線程B到主內(nèi)存中去讀取線程A之前已經(jīng)更新過的共享變量浅碾。
線程對共享變量的所有操作都必須在自己的本地內(nèi)存中進行大州,不能直接從主內(nèi)存中讀取。
所以線程B并不是直接去主內(nèi)存中讀取共享變量的值垂谢,而是先在本地內(nèi)存B中找到這個共享變量厦画,發(fā)現(xiàn)這個共享變量已經(jīng)被更新了,然后本地內(nèi)存B去主內(nèi)存中讀取這個共享變量的新值滥朱,并拷貝到本地內(nèi)存B中根暑,最后線程B再讀取本地內(nèi)存B中的新值。
JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互徙邻,來提供內(nèi)存可見性保證排嫌。
volatile: 共享變量的可見性 & 禁止指令重排序
sychronized: 可見性 & 原子性 (互斥性)
low level --> Java的內(nèi)存屏障來實現(xiàn)內(nèi)存的可見性以及禁止重排序。
6.2.3 JMM與Java內(nèi)存區(qū)域劃分的區(qū)別與聯(lián)系
區(qū)別
兩者是不同的概念層次缰犁。JMM是抽象的淳地,他是用來描述一組規(guī)則怖糊,通過這個規(guī)則來控制各個變量的訪問方式,圍繞原子性颇象、有序性伍伤、可見性等展開的。而Java運行時內(nèi)存的劃分是具體的遣钳,是JVM運行Java程序時扰魂,必要的內(nèi)存劃分。聯(lián)系
都存在私有數(shù)據(jù)區(qū)域和共享數(shù)據(jù)區(qū)域蕴茴。一般來說劝评,JMM中的主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,他是包含了堆和方法區(qū)倦淀;同樣蒋畜,JMM中的本地內(nèi)存屬于私有數(shù)據(jù)區(qū)域,包含了程序計數(shù)器晃听、本地方法棧百侧、虛擬機棧。
7.重排序與happens-before
7.1 什么是重排序
執(zhí)行程序的時候能扒,為了提高性能佣渴,編譯器和處理器常會對指令進行重排。
why ↑ performance初斑?
- 流水線技術(shù): 每一個指令都會包含多個步驟辛润,每個步驟可能使用不同的硬件。原理是指令1還沒有執(zhí)行完见秤,就可以開始執(zhí)行指令2砂竖,而不用等到指令1執(zhí)行結(jié)束之后再執(zhí)行指令2,這樣就大大提高了效率鹃答。
- 流水線害怕中斷乎澄,恢復(fù)中斷的代價是比較大的,指令重排就是減少中斷的一種技術(shù)测摔。
示例
a = b + c;
d = e - f ;
先加載b置济、c(注意,即有可能先加載b锋八,也有可能先加載c)浙于,但是在執(zhí)行add(b,c)的時候,需要等待b挟纱、c裝載結(jié)束才能繼續(xù)執(zhí)行羞酗,也就是增加了停頓,那么后面的指令也會依次有停頓,這降低了計算機的執(zhí)行效率紊服。
為了減少這個停頓檀轨,我們可以先加載e和f,然后再去加載add(b,c),這樣做對程序(串行)是沒有影響的,但卻減少了停頓胸竞。
指令重排對于提高CPU處理性能十分必要。雖然由此帶來了亂序的問題裤园,但是這點犧牲是值得的撤师。
指令重排分為以下三種:
- 編譯器優(yōu)化重排
編譯器在不改變單線程程序語義的前提下剂府,可以重新安排語句的執(zhí)行順序拧揽。 - 指令并行重排
現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個執(zhí)行的語句無需依賴前面執(zhí)行的語句的結(jié)果)腺占,處理器可以改變語句對應(yīng)的機器指令的執(zhí)行順序淤袜。 - 內(nèi)存系統(tǒng)重排
由于處理器使用緩存和讀寫緩存沖區(qū),這使得加載(load)和存儲(store)操作看上去可能是在亂序執(zhí)行衰伯,因為三級緩存的存在铡羡,導(dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時間差。
指令重排可以保證串行語義一致意鲸,但是沒有義務(wù)保證多線程間的語義也一致烦周。所以在多線程下,指令重排序可能會導(dǎo)致一些問題怎顾。
7.2 順序一致性模型與JMM保證
順序一致性模型是一個理論參考模型读慎,內(nèi)存模型在設(shè)計的時候都會以順序一致性內(nèi)存模型作為參考。
7.2.1 數(shù)據(jù)競爭與順序一致性
程序未正確同步時 --> 可能存在數(shù)據(jù)競爭 --> 運行結(jié)果充滿了不確定性
數(shù)據(jù)競爭:在一個線程中寫一個變量槐雾,在另一個線程讀同一個變量夭委,并且寫和讀沒有通過同步來排序。
Java內(nèi)存模型(JMM)對于正確同步多線程程序內(nèi)存一致性做了以下保證:
如果程序是正確同步的募强,程序的執(zhí)行將具有順序一致性株灸。 即程序的執(zhí)行結(jié)果和該程序在順序一致性模型中執(zhí)行的結(jié)果相同
同步包括了使用volatile、final擎值、synchronized等關(guān)鍵字來實現(xiàn)多線程下的同步慌烧。
7.2.2 順序一致性模型
理想化的理論參考模型 --> 極強的內(nèi)存可見性保證
此模型的兩大特性:
- 線程所有的操作必須按照程序的順序(即Java代碼的順序)來執(zhí)行
- 不管程序是否同步,所有線程都只能看到一個單一的操作執(zhí)行順序鸠儿。即在順序一致性模型中屹蚊,每個操作必須是原子性的,且立刻對所有線程可見捆交。
eg: 兩個線程:線程A有3個操作淑翼,他們在程序中的順序是A1->A2->A3,線程B也有3個操作品追,B1->B2->B3玄括。
正確使用了同步+順序一致性模型
沒有使用同步+順序一致性模型
但是JMM沒有這樣的保證
比如,在當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中肉瓦,在沒有刷新到主內(nèi)存之前遭京,這個寫操作僅對當(dāng)前線程可見胃惜;從其他線程的角度來觀察,這個寫操作根本沒有被當(dāng)前線程所執(zhí)行哪雕。只有當(dāng)前線程把本地內(nèi)存中寫過的數(shù)據(jù)刷新到主內(nèi)存之后船殉,這個寫操作才對其他線程可見。在這種情況下斯嚎,當(dāng)前線程和其他線程看到的執(zhí)行順序是不一樣的利虫。
7.2.3 JMM中同步程序的順序一致性效果
順序一致模型:所有操作完全按照程序順序串行執(zhí)行。
JMM中:臨界區(qū)內(nèi)(同步塊或同步方法中)--> 代碼可重排序(但不允許臨界區(qū)內(nèi)的代碼“逃逸”到臨界區(qū)之外堡僻,因為會破壞鎖的內(nèi)存語義)
JMM中糠惫,例如A在臨界區(qū)做了重排,∵鎖钉疫,其他線程看不到硼讽,∴性能提高了,一樣的執(zhí)行結(jié)果
JMM退出臨界區(qū)和進入臨界區(qū)做特殊的處理牲阁,使得在臨界區(qū)內(nèi)程序獲得與順序一致性模型相同的內(nèi)存視圖固阁。
總而言之,JMM的具體實現(xiàn)方針是:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下城菊,盡量為編譯期和處理器的優(yōu)化打開方便之門备燃。
7.2.4 JMM中未同步程序的順序一致性效果
未同步的多線程程序 --> 最小安全性: 線程讀取的值 -- 1.之前寫入的值 2. 默認值 --> 實現(xiàn)方式: JVM在堆上分配對象,先清空space役电,再分配對象(同步的操作)
JMM沒有保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性中執(zhí)行結(jié)果一致赚爵。因為如果要保證執(zhí)行結(jié)果一致,那么JMM需要禁止大量的優(yōu)化法瑟,對程序的執(zhí)行性能會產(chǎn)生很大的影響冀膝。
未同步程序在JMM和順序一致性內(nèi)存模型中的執(zhí)行特性的差異:
- 順序一致性保證單線程內(nèi)的操作按程序的順序執(zhí)行;JMM不保證單線程內(nèi)操作會按程序順序執(zhí)行(∵重排序霎挟,但是JMM保證單線程下的重排序不影響執(zhí)行結(jié)果)
- 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序窝剖,而JMM不保證所有程序能看到一樣的操作執(zhí)行順序(∵JMM不保證所有操作立即可見)
- JMM不保證對64位的long型和double型變量寫操作具有原子性,而順序一致性模型保證要保證對所有的內(nèi)存讀寫都具有原子性
7.3 happens-before
7.3.1 什么是happens-before?
程序員需要JMM提供一個強的內(nèi)存模型來編寫代碼酥夭;另一方面赐纱,編譯器和處理器希望JMM對他們束縛越少越好,這樣就可以更多的做優(yōu)化來提高性能熬北,希望弱的內(nèi)存模型疙描。
JMM考慮兩種需求,找到了平衡點讶隐,對于編譯器和處理器來說起胰,只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確同步了的多線程程序),編譯器和處理器怎么優(yōu)化都行巫延。
對于程序員來說效五,JMM提供了happens-before規(guī)則地消,簡單易懂,并且提供了足夠強的內(nèi)存可見性保證畏妖。
JMM使用happens-before的概念來定制兩個操作之間的執(zhí)行順序脉执。這兩個操作可以在一個線程以內(nèi),也可以是不同的線程之間戒劫。因此半夷,JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證。
happens-before關(guān)系的定義如下:
- 如果一個操作happens-before另一個操作谱仪,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見玻熙,而且第一個操作的執(zhí)行順序排在第二個操作之前否彩。
- 兩個操作之間存在happens-before關(guān)系疯攒,并不意味著Java平臺的具體實現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果列荔,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致敬尺,那么JMM也允許這樣的重排序。
如果操作A happens-before操作B贴浙,那么操作A在內(nèi)存上所做的操作對操作B都是可見的砂吞,不管它們在不在一個線程。
7.3.2 天然的happens-before關(guān)系
- 程序順序規(guī)則:一個線程中的每一個操作崎溃,happens-before于該線程中的任意后續(xù)操作蜻直。
- 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖袁串。
- volatile變量規(guī)則:對一個volatile域的寫概而,happens-before于任意后續(xù)對這個volatile域的讀。
- 傳遞性:如果A happens-before B囱修,且B happens-before C赎瑰,那么A happens-before C。
- start規(guī)則:如果線程A執(zhí)行操作ThreadB.start()啟動線程B破镰,那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作餐曼、
- join規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回鲜漩。
8. volatile
8.1 幾個基本概念
8.1.1 內(nèi)存可見性
內(nèi)存可見性源譬,指的是線程之間的可見性,當(dāng)一個線程修改了共享變量時孕似,另一個線程可以讀取到這個修改后的值踩娘。
8.1.2 重排序
為了優(yōu)化程序性能,對原有指令順序進行重新排序鳞青。重排序可能發(fā)生在多個階段霸饲,如編譯重排为朋、CPU重排等
8.1.3 happens-before規(guī)則
程序員寫代碼時遵守happens-before規(guī)則,JMM能保證指令在多線程之間的順序性符合程序員的預(yù)期厚脉。
8.2 volatile的內(nèi)存語義
volatile的兩個功能:
- 保證變量的內(nèi)存可見性
- 禁止volatile變量與普通變量重排序
8.2.1 內(nèi)存可見性
示例代碼
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // step 1
flag = true; // step 2
}
public void reader() {
if (flag) { // step 3
System.out.println(a); // step 4
}
}
}
內(nèi)存可見性:指的是當(dāng)一個線程對volatile修飾的變量進行寫操作(比如step 2)時习寸,JMM會立即把該線程對應(yīng)的本地內(nèi)存中的共享變量的值刷新到主內(nèi)存;當(dāng)一個線程對volatile修飾的變量進行讀操作(比如step 3)時傻工,JMM會把立即該線程對應(yīng)的本地內(nèi)存置為無效霞溪,從主內(nèi)存中讀取共享變量的值。
volatile與鎖具有相同的內(nèi)存效果中捆,volatile變量的寫和鎖釋放具有相同的內(nèi)存語義(立即把更新刷新到內(nèi)存中)鸯匹,volatile變量的讀和鎖的獲取具有相同的內(nèi)存語義(立即從內(nèi)存中獲取值)。
假設(shè)在時間線上泄伪,線程A先自行方法writer方法殴蓬,線程B后執(zhí)行reader方法。那必然會有下圖:
而如果flag變量沒有用volatile修飾蟋滴,在step 2染厅,線程A的本地內(nèi)存里面的變量就不會立即更新到主內(nèi)存,那隨后線程B也同樣不會去主內(nèi)存拿最新的值津函,仍然使用線程B本地內(nèi)存緩存的變量的值a = 0肖粮,flag = false。
8.2.2 禁止重排序
在JSR-133之前的舊的Java內(nèi)存模型中尔苦,是允許volatile變量與普通變量重排序的涩馆。上面的例子可能會被重排為:
- 線程A寫volatile變量,step 2允坚,設(shè)置flag為true魂那;
- 線程B讀同一個volatile,step 3屋讶,讀取到flag為true冰寻;
- 線程B讀普通變量,step 4皿渗,讀取到 a = 0斩芭;
- 線程A修改普通變量,step 1乐疆,設(shè)置 a = 1划乖;
因此,如果volatile變量與普通變量發(fā)生了重排序挤土,雖然volatile變量能保證內(nèi)存可見性琴庵,也可能導(dǎo)致普通變量讀取錯誤。
所以,舊的內(nèi)存模型中迷殿,volatile的寫-讀不能與鎖的釋放-獲取具有相同的內(nèi)存語義儿礼。為了提供更輕量級的線程通信機制,JSR-133專家組決定增強volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對volatile變量與普通變量的重排序庆寺。
限制處理器對指令進行重排: 使用內(nèi)存屏障
硬件層面蚊夫,內(nèi)存屏障分兩種:讀屏障(Load Barrier)和寫屏障(Store Barrier)。內(nèi)存屏障的兩個作用:
- 阻止屏障兩側(cè)的指令重排
- 強制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存懦尝,或者讓緩存中相應(yīng)的數(shù)據(jù)失效知纷。
編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理數(shù)據(jù)的重排序陵霉,編譯器選擇了一個比較保守的JMM內(nèi)存屏障插入策略琅轧,這個策略是:
- 在每個volatile寫操作前插入一個StoreStore屏障;
- 在每個volatile寫操作后插入一個StoreLoad屏障踊挠;
- 在每個volatile讀操作后插入一個LoadLoad屏障乍桂;
- 在每個volatile讀操作后再插入一個LoadStore屏障。
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2止毕,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前模蜡,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2扁凛,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見闯传。
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2谨朝,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢甥绿。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2字币,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見共缕。它的開銷是四種屏障中最大的(沖刷寫緩沖器洗出,清空無效化隊列)。在大多數(shù)處理器的實現(xiàn)中图谷,這個屏障是個萬能屏障翩活,兼具其它三種內(nèi)存屏障的功能
volatile與普通變量的重排序規(guī)則:
- 如果第一個操作是volatile讀,那無論第二個操作是什么便贵,都不能重排序菠镇。(因為要把主存中的數(shù)據(jù)刷新到線程的工作空間中)
- 如果第二個操作是volatile寫,那無論第一個操作是什么承璃,都不能重排序(因為要將線程本地數(shù)據(jù)刷新到主存中)
- 如果第一個操作是volatile寫利耍,第二個操作是volatile讀,那不能重排序
8.3 volatile的用途
volatile可以保證內(nèi)存可見性且禁止重排序。
內(nèi)存可見性:volatile有著與鎖相同的內(nèi)存語義隘梨,所以可以作為一個“輕量級”的鎖來使用程癌。但由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖可以保證整個臨界區(qū)代碼的執(zhí)行具有原子性轴猎。所以在功能上席楚,鎖比volatile更強大;在性能上税稼,volatile更有優(yōu)勢烦秩。
禁止重排序:單例模式,其中有一種實現(xiàn)方式是“雙重鎖檢查”
public class Singleton {
private static Singleton instance; // 不使用volatile關(guān)鍵字
// 雙重鎖檢驗
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
如果變量聲明不使用volatile關(guān)鍵字郎仆,可能發(fā)生錯誤只祠,可能會被重排序:
instance = new Singleton(); // 第10行
// 可以分解為以下三個步驟
1 memory=allocate();// 分配內(nèi)存 相當(dāng)于c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設(shè)置s指向剛分配的地址
// 上述三個步驟可能會被重排序為 1-3-2,也就是:
1 memory=allocate();// 分配內(nèi)存 相當(dāng)于c的malloc
3 s=memory //設(shè)置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象
而一旦假設(shè)發(fā)生了這樣的重排序扰肌,比如線程A在第10行執(zhí)行了步驟1和步驟3抛寝,但是步驟2還沒有執(zhí)行完。這個時候另一個線程B執(zhí)行到了第7行曙旭,它會判定instance不為空盗舰,然后直接返回了一個未初始化完成的instance!
所以JSR-133對volatile做了增強后桂躏,volatile的禁止重排序功能還是非常有用的钻趋。
9. synchronized與鎖
Java多線程的鎖都是基于對象的,Java中每個對象都可以作為一個鎖剂习。
類鎖也是對象鎖蛮位,Java類只有一個Class對象(可以有多個實例對象,多個實例共享這個Class對象)鳞绕,而Class對象也是特殊的Java對象失仁。所以我們常說的類鎖,其實就是Class對象的鎖们何。
9.1 Synchronized關(guān)鍵字
Synchronized: 同步關(guān)鍵字舒憾,用來給一段代碼或一個方法上鎖
上鎖的三種方式:
// 關(guān)鍵字在實例方法上彬呻,鎖為當(dāng)前實例
public synchronized void instanceLock() {
// code
}
// 關(guān)鍵字在靜態(tài)方法上哑蔫,鎖為當(dāng)前Class對象
public static synchronized void classLock() {
// code
}
// 關(guān)鍵字在代碼塊上案铺,鎖為括號里面的對象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
臨界區(qū):某一塊代碼區(qū)域,在同一時刻只能由一個線程執(zhí)行烘苹。如果synchronized關(guān)鍵字在方法上,那臨界區(qū)就是整個方法內(nèi)部片部。而如果是使用synchronized代碼塊镣衡,那臨界區(qū)就指的是代碼塊內(nèi)部的區(qū)域霜定。
鎖為當(dāng)前實例的另一種寫法:
// 關(guān)鍵字在實例方法上,鎖為當(dāng)前實例
public synchronized void instanceLock() {
// code
}
// 關(guān)鍵字在代碼塊上廊鸥,鎖為括號里面的對象
public void blockLock() {
synchronized (this) {
// code
}
}
鎖為當(dāng)前class對象的另一種寫法:
// 關(guān)鍵字在靜態(tài)方法上望浩,鎖為當(dāng)前Class對象
public static synchronized void classLock() {
// code
}
// 關(guān)鍵字在代碼塊上,鎖為括號里面的對象
public void blockLock() {
synchronized (this.getClass()) {
// code
}
}
9.2 幾種鎖
Java 6 為了減少獲得鎖和釋放鎖帶來的性能消耗惰说,引入了“偏向鎖”和“輕量級鎖“磨德。在Java 6 以前,所有的鎖都是”重量級“鎖吆视。所以在Java 6 及其以后典挑,一個對象其實有四種鎖狀態(tài),它們級別由低到高依次是:
- 無鎖狀態(tài)
- 偏向鎖狀態(tài)
- 輕量級鎖狀態(tài)
- 重量級鎖狀態(tài)
無鎖就是沒有對資源進行鎖定啦吧,任何線程都可以嘗試去修改它您觉。
競爭升級 --> 鎖容易升級,鎖降級條件比較苛刻授滓,鎖降級發(fā)生在Stop The World期間琳水,當(dāng)JVM進入安全點的時候,會檢查是否有閑置的鎖般堆,然后進行降級在孝。
9.2.1 Java對象頭
Java的鎖都是基于對象的,一個“對象”的鎖信息存放在對象頭中淮摔。
每個Java對象都有對象頭私沮。如果是非數(shù)組類型,則用2個字寬來存儲對象頭噩咪,如果是數(shù)組顾彰,則會用3個字寬來存儲對象頭。在32位處理器中胃碾,一個字寬是32位;在64位虛擬機中筋搏,一個字寬是64位仆百。對象頭的內(nèi)容如下表:
當(dāng)對象狀態(tài)為偏向鎖時,Mark Word存儲的是偏向的線程ID奔脐;當(dāng)狀態(tài)為輕量級鎖時俄周,Mark Word存儲的是指向線程棧中Lock Record的指針;當(dāng)狀態(tài)為重量級鎖時髓迎,Mark Word為指向堆中的monitor對象的指針峦朗。
9.2.2 偏向鎖
大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得排龄,于是引入了偏向鎖波势。
偏向鎖會偏向于第一個訪問鎖的線程,如果在接下來的運行過程中,該鎖沒有被其他的線程訪問尺铣,則持有偏向鎖的線程將永遠不需要觸發(fā)同步拴曲。也就是說,偏向鎖在資源無競爭情況下消除了同步語句凛忿,連CAS操作都不做了澈灼,提高了程序的運行性能。
實現(xiàn)原理
一個線程在第一次進入同步塊時店溢,會在對象頭和棧幀中的鎖記錄里存儲鎖的偏向的線程ID叁熔。當(dāng)下次該線程進入這個同步塊時,會去檢查鎖的Mark Word里面是不是放的自己的線程ID床牧。
如果是荣回,表明該線程已經(jīng)獲得了鎖,以后該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖 叠赦;如果不是驹马,就代表有另一個線程來競爭這個偏向鎖。這個時候會嘗試使用CAS來替換Mark Word里面的線程ID為新線程的ID除秀,這個時候要分兩種情況:
- 成功糯累,表示之前的線程不存在了, Mark Word里面的線程ID為新線程的ID册踩,鎖不會升級泳姐,仍然為偏向鎖;
- 失敗暂吉,表示之前的線程仍然存在胖秒,那么暫停之前的線程,設(shè)置偏向鎖標(biāo)識為0慕的,并設(shè)置鎖標(biāo)志位為00阎肝,升級為輕量級鎖,會按照輕量級鎖的方式進行競爭鎖肮街。
撤銷偏向鎖
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制风题,所以當(dāng)其他線程嘗試競爭偏向鎖時, 持有偏向鎖的線程才會釋放鎖嫉父。
偏向鎖升級成輕量級鎖時沛硅,會暫停擁有偏向鎖的線程,重置偏向鎖標(biāo)識绕辖,這個過程看起來容易摇肌,實則開銷還是很大的,大概的過程如下:
- 在一個安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)停止擁有鎖的線程仪际。
- 遍歷線程棧围小,如果存在鎖記錄的話昵骤,需要修復(fù)鎖記錄和Mark Word,使其變成無鎖狀態(tài)吩抓。
- 喚醒被停止的線程涉茧,將當(dāng)前鎖升級成輕量級鎖。
若應(yīng)用程序中所有鎖通常處于競爭狀態(tài)疹娶,偏向鎖是一種累贅伴栓,可以一開始把偏向鎖這個默認功能關(guān)閉:
-XX:UseBiasedLocking=false
9.2.3 輕量級鎖
多個線程在不同時段獲取同一把鎖,即不存在鎖競爭的情況雨饺,也就沒有線程阻塞钳垮。針對這種情況,JVM采用輕量級鎖來避免線程的阻塞與喚醒额港。
輕量級鎖的加鎖
JVM會為每個線程在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間饺窿,我們稱為Displaced Mark Word。如果一個線程獲得鎖的時候發(fā)現(xiàn)是輕量級鎖移斩,會把鎖的Mark Word復(fù)制到自己的Displaced Mark Word里面肚医。
然后線程嘗試用CAS將鎖的Mark Word替換為指向鎖記錄的指針。如果成功向瓷,當(dāng)前線程獲得鎖肠套,如果失敗,表示Mark Word已經(jīng)被替換成了其他線程的鎖記錄猖任,說明在與其它線程競爭鎖你稚,當(dāng)前線程就嘗試使用自旋來獲取鎖。
自旋是需要消耗CPU的朱躺,如果一直獲取不到鎖的話刁赖,那該線程就一直處在自旋狀態(tài),白白浪費CPU資源长搀。解決這個問題最簡單的辦法就是指定自旋的次數(shù)宇弛,例如讓其循環(huán)10次,如果還沒獲取到鎖就進入阻塞狀態(tài)源请。
但是JDK采用了更聰明的方式——適應(yīng)性自旋涯肩,簡單來說就是線程如果自旋成功了,則下次自旋的次數(shù)會更多巢钓,如果自旋失敗了,則自旋的次數(shù)就會減少疗垛。
自旋也不是一直進行下去的症汹,如果自旋到一定程度(和JVM、操作系統(tǒng)相關(guān))贷腕,依然沒有獲取到鎖背镇,稱為自旋失敗咬展,那么這個線程會阻塞。同時這個鎖就會升級成重量級鎖瞒斩。
輕量級鎖的釋放
在釋放鎖時破婆,當(dāng)前線程會使用CAS操作將Displaced Mark Word的內(nèi)容復(fù)制回鎖的Mark Word里面。如果沒有發(fā)生競爭胸囱,那么這個復(fù)制的操作會成功祷舀。如果有其他線程因為自旋多次導(dǎo)致輕量級鎖升級成了重量級鎖,那么CAS操作會失敗烹笔,此時會釋放鎖并喚醒被阻塞的線程裳扯。
9.2.4 重量級鎖
重量級鎖依賴于操作系統(tǒng)的互斥量(mutex) 實現(xiàn)的,而操作系統(tǒng)中線程間狀態(tài)的轉(zhuǎn)換需要相對比較長的時間谤职,所以重量級鎖效率很低饰豺,但被阻塞的線程不會消耗CPU。
前面說到允蜈,每一個對象都可以當(dāng)做一個鎖冤吨,當(dāng)多個線程同時請求某個對象鎖時,對象鎖會設(shè)置幾種狀態(tài)用來區(qū)分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊列
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List
Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set
OnDeck:任何時刻最多只能有一個線程正在競爭鎖饶套,該線程稱為OnDeck
Owner:獲得鎖的線程稱為Owner
!Owner:釋放鎖的線程
當(dāng)一個線程嘗試獲得鎖時漩蟆,如果該鎖已經(jīng)被占用,則會將該線程封裝成一個ObjectWaiter對象插入到Contention List的隊列的隊首凤跑,然后調(diào)用park函數(shù)掛起當(dāng)前線程爆安。
當(dāng)線程釋放鎖時,會從Contention List或EntryList中挑選一個線程喚醒仔引,被選中的線程叫做Heir presumptive即假定繼承人扔仓,假定繼承人被喚醒后會嘗試獲得鎖,但synchronized是非公平的咖耘,所以假定繼承人不一定能獲得鎖翘簇。這是因為對于重量級鎖,線程先自旋嘗試獲得鎖儿倒,這樣做的目的是為了減少執(zhí)行操作系統(tǒng)同步操作帶來的開銷版保。如果自旋不成功再進入等待隊列。這對那些已經(jīng)在等待隊列中的線程來說夫否,稍微顯得不公平彻犁,還有一個不公平的地方是自旋線程可能會搶占了Ready線程的鎖。
如果線程獲得鎖后調(diào)用Object.wait方法凰慈,則會將線程加入到WaitSet中汞幢,當(dāng)被Object.notify喚醒后,會將線程從WaitSet移動到Contention List或EntryList中去微谓。需要注意的是森篷,當(dāng)調(diào)用一個鎖對象的wait或notify方法時输钩,如當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。
9.2.5 總結(jié)鎖的升級流程
每一個線程在準(zhǔn)備獲取共享資源時: 第一步仲智,檢查MarkWord里面是不是放的自己的ThreadId ,如果是买乃,表示當(dāng)前線程是處于 “偏向鎖” 。
第二步钓辆,如果MarkWord不是自己的ThreadId剪验,鎖升級,這時候岩馍,用CAS來執(zhí)行切換碉咆,新的線程根據(jù)MarkWord里面現(xiàn)有的ThreadId,通知之前線程暫停蛀恩,之前線程將Markword的內(nèi)容置為空疫铜。
第三步,兩個線程都把鎖對象的HashCode復(fù)制到自己新建的用于存儲鎖的記錄空間双谆,接著開始通過CAS操作壳咕, 把鎖對象的MarKword的內(nèi)容修改為自己新建的記錄空間的地址的方式競爭MarkWord。
第四步顽馋,第三步中成功執(zhí)行CAS的獲得資源谓厘,失敗的則進入自旋 。
第五步寸谜,自旋的線程在自旋過程中竟稳,成功獲得資源(即之前獲的資源的線程執(zhí)行完成并釋放了共享資源),則整個狀態(tài)依然處于 輕量級鎖的狀態(tài)熊痴,如果自旋失敗 他爸。
第六步,進入重量級鎖的狀態(tài)果善,這個時候诊笤,自旋的線程進行阻塞,等待之前線程執(zhí)行完成并喚醒自己巾陕。
9.2.6 各種鎖優(yōu)缺點對比
10. CAS與原子操作
10.1 樂觀鎖與悲觀鎖的概念
鎖可以從不同的角度分類讨跟,悲觀鎖和樂觀鎖是一種分類方式
悲觀鎖:
悲觀鎖就是常說的鎖。對于悲觀鎖來說鄙煤,它總是認為每次訪問共享資源時會發(fā)生沖突晾匠,所以必須對每次數(shù)據(jù)操作加上鎖,以保證臨界區(qū)的程序同一時間只能有一個線程在執(zhí)行梯刚。
樂觀鎖:
樂觀鎖又稱為“無鎖”混聊。樂觀鎖總是假設(shè)對共享資源的訪問沒有沖突,線程可以不停地執(zhí)行,無需加鎖也無需等待句喜。而一旦多個線程發(fā)生沖突,樂觀鎖通常是使用一種稱為CAS的技術(shù)來保證線程執(zhí)行的安全性沟于。
無鎖操作中沒有鎖咳胃,∴不會出現(xiàn)死鎖,樂觀鎖天生免疫死鎖
樂觀鎖多用于“讀多寫少“的環(huán)境旷太,避免頻繁加鎖影響性能展懈;而悲觀鎖多用于”寫多讀少“的環(huán)境,避免頻繁失敗和重試影響性能供璧。
10.2 CAS的概念
CAS: 比較并交換 - Compare And Swap
CAS中有三個值:
- V:要更新的變量(var)
- E:預(yù)期值(expected) -- 本質(zhì)上是舊值
- N:新值(new)
比較并交換的過程:
V==E存崖?V的的值設(shè)為N:放棄更新
CAS是一種原子操作,是一種系統(tǒng)原語睡毒,是一條CPU的原子指令来惧,從CPU層面保證它的原子性.
當(dāng)多個線程同時使用CAS操作一個變量時,只有一個會勝出演顾,并成功更新供搀,其余均會失敗,但失敗的線程并不會被掛起钠至,僅是被告知失敗葛虐,并且允許再次嘗試,當(dāng)然也允許失敗的線程放棄操作棉钧。
10.3 Java實現(xiàn)CAS的原理 - Unsafe類
Java中屿脐,若方法是native的,那Java不負責(zé)實現(xiàn)它宪卿,而是交給底層的JVM使用c或者c++實現(xiàn)的诵。
在Java中,在sun.misc包中有一個Unsafe類愧捕,它里面是一些native方法奢驯,其中有關(guān)CAS的
boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
Unsafe中對CAS的實現(xiàn)是用c++寫的,具體實現(xiàn)和操作系統(tǒng)次绘,CPU都有關(guān)系瘪阁。
Linux的X86下主要是通過cmpxchgl這個指令在CPU級完成CAS操作的,但在多處理器情況下必須使用lock指令加鎖來完成邮偎。不同操作系統(tǒng)和處理器實現(xiàn)會有所不同管跺。
10.4 原子操作 - AtomicInteger類源碼簡析
Java會使用Unsafe類的幾個支持CAS的方法實現(xiàn)原子操作。
JDK提供了一些用于原子操作的類禾进,在java.util.concurrent.atomic包下面豁跑。這些類大概的用途:
- 原子更新基本類型
- 原子更新數(shù)組
- 原子更新引用
- 原子更新字段(屬性)
eg: AtomicInteger類的 getAndAdd(int delta)方法
該方法的源碼:
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
這里的U是一個Unsafe對象:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
所以AtomicInteger類的 getAndAdd(int delta)方法是
調(diào)用Unsafe類的方法來實現(xiàn)的:
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
對象o是this,也就是一個AtomicInteger對象泻云。然后offset是一個常量VALUE艇拍。這個常量是在AtomicInteger類中聲明的狐蜕,得到了一個對象字段偏移量:
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
方法體中聲明了一個v,也就是要返回的值卸夕。從getAndAddInt來看层释,它返回的應(yīng)該是原來的值,而新的值的v + delta快集。
這里使用了do-while循環(huán)贡羔,目的是保證循環(huán)體內(nèi)的語句至少執(zhí)行一次,這樣可以保證return的值v是期望的值
循環(huán)體的條件是一個CAS方法个初, 它是不斷嘗試去用CAS更新 -- 最終調(diào)用了CAS native方法:
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
使用weakCompareAndSet操作的原因:weakCompareAndSet操作僅保留了volatile自身變量的特性乖寒,而出去了happens-before規(guī)則帶來的內(nèi)存語義。也就是說院溺,weakCompareAndSet無法保證處理操作目標(biāo)的volatile變量外的其他變量的執(zhí)行順序( 編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序 )楣嘁,同時也無法保證這些變量的可見性。這在一定程度上可以提高性能覆获。
10.5 CAS實現(xiàn)原子操作的三大問題
10.5.1 ABA問題
A -> B -> A马澈, CAS無法檢查更新了兩次
解決思路:變量前追加版本號或時間戳,從JDK 1.5開始弄息,JDK的atomic包里提供了一個類AtomicStampedReference類來解決ABA問題痊班。
這個類的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志摹量,如果二者都相等涤伐,才使用CAS設(shè)置為新的值和標(biāo)志。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
10.5.2 循環(huán)時間長開銷大
CAS多與自旋結(jié)合缨称。如果自旋CAS長時間不成功凝果,會占用大量的CPU資源。
解決思路是讓JVM支持處理器提供的pause指令睦尽。
pause指令能讓自旋失敗時cpu睡眠一小段時間再繼續(xù)自旋器净,從而使得讀操作的頻率低很多,為解決內(nèi)存順序沖突而導(dǎo)致的CPU流水線重排的代價也會小很多。
10.5.3 只能保證一個共享變量的原子操作
兩種方案:
- 使用JDK 1.5開始就提供的AtomicReference類保證對象之間的原子性当凡,把多個變量放到一個對象里面進行CAS操作山害;
- 使用鎖。鎖內(nèi)的臨界區(qū)代碼可以保證只有當(dāng)前線程能操作沿量。
11. AQS
11.1 AQS簡介
AQS: AbstractQuenedSychronizer浪慌,抽象隊列同步器
- 抽象: 抽象類,只實現(xiàn)一些主要邏輯朴则,有些方法由子類實現(xiàn)
- 隊列: 使用先進先出(FIFO)隊列存儲數(shù)據(jù)
- 同步: 實現(xiàn)了同步的功能
AQS是一個用來構(gòu)建鎖和同步器的框架权纤,使用AQS能簡單且高效地構(gòu)造出應(yīng)用廣泛的同步器,比如ReentrantLock,Semaphore汹想,ReentrantReadWriteLock外邓,SynchronousQueue,F(xiàn)utureTask等等皆是基于AQS的欧宜。
11.2 AQS的數(shù)據(jù)結(jié)構(gòu)
AQS內(nèi)部使用了一個volatile的變量state來作為資源的標(biāo)識坐榆。同時定義了幾個獲取和改版state的protected方法,子類可以覆蓋這些方法來實現(xiàn)自己的邏輯:
getState()
setState()
compareAndSetState()
這三種叫做均是原子操作,其中compareAndSetState的實現(xiàn)依賴于Unsafe的compareAndSwapInt()方法贞盯。
而AQS類本身實現(xiàn)的是一些排隊和阻塞的機制耘子,比如具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等)。它內(nèi)部使用了一個先進先出(FIFO)的雙端隊列焰宣,并使用了兩個指針head和tail用于標(biāo)識隊列的頭部和尾部。其數(shù)據(jù)結(jié)構(gòu)如圖:
但它并不是直接儲存線程,而是儲存擁有線程的Node節(jié)點挂绰。
11.3 資源共享模式
資源有兩種共享模式:
- 獨占模式(Exclusive):資源是獨占的,一次只能一個線程獲取服赎。如ReentrantLock葵蒂。
- 共享模式(Share):同時可以被多個線程獲取,具體的資源個數(shù)可以通過參數(shù)指定重虑。如Semaphore/CountDownLatch践付。
一般情況下,子類只需要根據(jù)需求實現(xiàn)其中一種模式缺厉,當(dāng)然也有同時實現(xiàn)兩種模式的同步類永高,如ReadWriteLock。
//AQS中關(guān)于這兩種資源共享模式的定義源碼
static final class Node {
// 標(biāo)記一個結(jié)點(對應(yīng)的線程)在共享模式下等待
static final Node SHARED = new Node();
// 標(biāo)記一個結(jié)點(對應(yīng)的線程)在獨占模式下等待
static final Node EXCLUSIVE = null;
// waitStatus的值提针,表示該結(jié)點(對應(yīng)的線程)已被取消
static final int CANCELLED = 1;
// waitStatus的值命爬,表示后繼結(jié)點(對應(yīng)的線程)需要被喚醒
static final int SIGNAL = -1;
// waitStatus的值,表示該結(jié)點(對應(yīng)的線程)在等待某一條件
static final int CONDITION = -2;
/*waitStatus的值辐脖,表示有資源可用饲宛,新head結(jié)點需要繼續(xù)喚醒后繼結(jié)點(共享模式下,多線程并發(fā)釋放資源嗜价,而head喚醒其后繼結(jié)點后艇抠,需要把多出來的資源留給后面的結(jié)點;設(shè)置新的head結(jié)點時炭剪,會繼續(xù)喚醒其后繼結(jié)點)*/
static final int PROPAGATE = -3;
// 等待狀態(tài)练链,取值范圍,-3奴拦,-2媒鼓,-1,0,1
volatile int waitStatus;
volatile Node prev; // 前驅(qū)結(jié)點
volatile Node next; // 后繼結(jié)點
volatile Thread thread; // 結(jié)點對應(yīng)的線程
Node nextWaiter; // 等待隊列里下一個等待條件的結(jié)點
// 判斷共享模式的方法
final boolean isShared() {
return nextWaiter == SHARED;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 其它方法忽略绿鸣,可以參考具體的源碼
}
// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
// 使用了Node的這個構(gòu)造函數(shù)
Node node = new Node(Thread.currentThread(), mode);
// 其它代碼省略
}
通過Node可以實現(xiàn)兩個隊列疚沐,一是通過prev和next實現(xiàn)CLH隊列(線程同步隊列,雙向隊列),二是nextWaiter實現(xiàn)Condition條件上的等待線程隊列(單向隊列)潮模,這個Condition主要用在ReentrantLock類中亮蛔。
11.4 AQS的主要方法源碼解析
AQS的設(shè)計是基于模板方法模式的,它有一些方法必須要子類去實現(xiàn)的擎厢,它們主要有:
- isHeldExclusively():該線程是否正在獨占資源究流。只有用到condition才需要去實現(xiàn)它。
- tryAcquire(int):獨占方式动遭。嘗試獲取資源芬探,成功則返回true,失敗則返回false厘惦。
- tryRelease(int):獨占方式偷仿。嘗試釋放資源,成功則返回true宵蕉,失敗則返回false酝静。
- tryAcquireShared(int):共享方式。嘗試獲取資源羡玛。負數(shù)表示失敱鹬恰;0表示成功缝左,但沒有剩余可用資源亿遂;正數(shù)表示成功,且有剩余資源渺杉。
- tryReleaseShared(int):共享方式蛇数。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待結(jié)點返回true是越,否則返回false耳舅。
根據(jù)源碼來分析一下獲取和釋放資源的主要邏輯:
11.4.1 獲取資源
獲取資源的入口是acquire(int arg)方法,arg是要獲取的資源的個數(shù)倚评,在獨占模式下始終為1浦徊。先來看看這個方法的邏輯:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先調(diào)用tryAcquire(arg)嘗試去獲取資源。前面提到了這個方法是在子類具體實現(xiàn)的天梧。
如果獲取資源失敗盔性,就通過addWaiter(Node.EXCLUSIVE)方法把這個線程插入到等待隊列中。其中傳入的參數(shù)代表要插入的Node是獨占式的呢岗。這個方法的具體實現(xiàn):
private Node addWaiter(Node mode) {
// 生成該線程對應(yīng)的Node節(jié)點
Node node = new Node(Thread.currentThread(), mode);
// 將Node插入隊列中
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 使用CAS嘗試冕香,如果成功就返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果等待隊列為空或者上述CAS失敗蛹尝,再自旋CAS插入
enq(node);
return node;
}
// 自旋CAS插入等待隊列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面的函數(shù)就是在隊列的尾部插入新的Node節(jié)點,由于AQS中會有多個線程爭奪資源的情況悉尾,會出現(xiàn)多個線程同時插入節(jié)點的操作突那,通過CAS自旋的方式保證了操作的線程安全性。
acquire(int arg)方法构眯,現(xiàn)在通過addWaiter方法愕难,已經(jīng)把一個Node放到等待隊列尾部了。而處于等待隊列的結(jié)點是從頭結(jié)點一個一個去獲取資源的惫霸。具體的是由acquireQueued方法實現(xiàn)的猫缭。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
// 如果node的前驅(qū)結(jié)點p是head,表示node是第二個結(jié)點壹店,就可以嘗試去獲取資源了
if (p == head && tryAcquire(arg)) {
// 拿到資源后饵骨,將head指向該結(jié)點。
// 所以head所指的結(jié)點茫打,就是當(dāng)前獲取到資源的那個結(jié)點或null。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果自己可以休息了妖混,就進入waiting狀態(tài)老赤,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
結(jié)點進入等待隊列后,調(diào)用park使他進入阻塞狀態(tài)制市,只有頭節(jié)點的線程處于活躍的狀態(tài)
獲取資源的方法除了acquire抬旺,還有下面的三個:
- acquireInterruptibly:申請可中斷的資源(獨占模式)
- acquireShared:申請共享模式的資源
- acquireSharedInterruptibly:申請可中斷的資源(共享模式)
11.4.2 釋放資源
釋放資源相對簡單一些,源碼:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 如果狀態(tài)是負數(shù)祥楣,嘗試把它設(shè)置為0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 得到頭結(jié)點的后繼結(jié)點head.next
Node s = node.next;
// 如果這個后繼結(jié)點為空或者狀態(tài)大于0
// 通過前面的定義我們知道开财,大于0只有一種可能,就是這個結(jié)點已被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 等待隊列中所有還有用的結(jié)點误褪,都向前移動
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果后繼結(jié)點不為空责鳍,
if (s != null)
LockSupport.unpark(s.thread);
}