2 - Java多線程原理篇

  1. Java內(nèi)存模型基礎(chǔ)知識
  2. 重排序與happens-before
  3. volatile
  4. synchronized與鎖
  5. CAS與原子操作
  6. AQS

6.Java內(nèi)存模型基礎(chǔ)知識

6.1 并發(fā)編程模型的兩個關(guān)鍵問題

  • 通信:交換信息的機制
  • 同步:控制不同線程間操作發(fā)生的相對順序

并發(fā)模型

  • 消息傳遞并發(fā)模型
  • 共享內(nèi)存并發(fā)模型

兩種模型之間區(qū)別:

兩種通信模式的區(qū)別

在Java中蝎宇,使用的是共享內(nèi)存并發(fā)模型

6.2 java內(nèi)存模型的抽象結(jié)構(gòu)

6.2.1 運行時內(nèi)存的劃分
運行時內(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的訪問速度)且警。


JMM控制的內(nèi)存可見性

Java線程之間的通信由Java內(nèi)存模型(簡稱JMM)控制,從抽象的角度來說礁遣,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系斑芜。

由圖可知:

  1. 所有的共享變量都存在主內(nèi)存中。
  2. 每個線程都保存了一份該線程使用到的共享變量的副本祟霍。
  3. 如果線程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初斑?

  1. 流水線技術(shù): 每一個指令都會包含多個步驟辛润,每個步驟可能使用不同的硬件。原理是指令1還沒有執(zhí)行完见秤,就可以開始執(zhí)行指令2砂竖,而不用等到指令1執(zhí)行結(jié)束之后再執(zhí)行指令2,這樣就大大提高了效率鹃答。
  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í)行特性的差異:

  1. 順序一致性保證單線程內(nèi)的操作按程序的順序執(zhí)行;JMM不保證單線程內(nèi)操作會按程序順序執(zhí)行(∵重排序霎挟,但是JMM保證單線程下的重排序不影響執(zhí)行結(jié)果)
  2. 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序窝剖,而JMM不保證所有程序能看到一樣的操作執(zhí)行順序(∵JMM不保證所有操作立即可見)
  3. 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)系的定義如下:

  1. 如果一個操作happens-before另一個操作谱仪,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見玻熙,而且第一個操作的執(zhí)行順序排在第二個操作之前否彩。
  2. 兩個操作之間存在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方法。那必然會有下圖:


volatile內(nèi)存示意圖

而如果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變量與普通變量重排序的涩馆。上面的例子可能會被重排為:

  1. 線程A寫volatile變量,step 2允坚,設(shè)置flag為true魂那;
  2. 線程B讀同一個volatile,step 3屋讶,讀取到flag為true冰寻;
  3. 線程B讀普通變量,step 4皿渗,讀取到 a = 0斩芭;
  4. 線程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)存屏障的兩個作用:

  1. 阻止屏障兩側(cè)的指令重排
  2. 強制把寫緩沖區(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屏障。
內(nèi)存屏障

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ī)則:

  1. 如果第一個操作是volatile讀,那無論第二個操作是什么便贵,都不能重排序菠镇。(因為要把主存中的數(shù)據(jù)刷新到線程的工作空間中)
  2. 如果第二個操作是volatile寫,那無論第一個操作是什么承璃,都不能重排序(因為要將線程本地數(shù)據(jù)刷新到主存中)
  3. 如果第一個操作是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),它們級別由低到高依次是:

  1. 無鎖狀態(tài)
  2. 偏向鎖狀態(tài)
  3. 輕量級鎖狀態(tài)
  4. 重量級鎖狀態(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)容如下表:

對象頭的內(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)識绕辖,這個過程看起來容易摇肌,實則開銷還是很大的,大概的過程如下:

  1. 在一個安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)停止擁有鎖的線程仪际。
  2. 遍歷線程棧围小,如果存在鎖記錄的話昵骤,需要修復(fù)鎖記錄和Mark Word,使其變成無鎖狀態(tài)吩抓。
  3. 喚醒被停止的線程涉茧,將當(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)缺點對比
各種鎖優(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 只能保證一個共享變量的原子操作

兩種方案:

  1. 使用JDK 1.5開始就提供的AtomicReference類保證對象之間的原子性当凡,把多個變量放到一個對象里面進行CAS操作山害;
  2. 使用鎖。鎖內(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)如圖:

AQS數(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:申請可中斷的資源(共享模式)
acquire流程.jpg
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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市兽间,隨后出現(xiàn)的幾起案子历葛,更是在濱河造成了極大的恐慌,老刑警劉巖嘀略,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恤溶,死亡現(xiàn)場離奇詭異,居然都是意外死亡帜羊,警方通過查閱死者的電腦和手機咒程,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讼育,“玉大人帐姻,你說我怎么就攤上這事稠集。” “怎么了卖宠?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵巍杈,是天一觀的道長。 經(jīng)常有香客問我扛伍,道長筷畦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任刺洒,我火速辦了婚禮鳖宾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘逆航。我一直安慰自己鼎文,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布因俐。 她就那樣靜靜地躺著拇惋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抹剩。 梳的紋絲不亂的頭發(fā)上撑帖,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機與錄音澳眷,去河邊找鬼胡嘿。 笑死,一個胖子當(dāng)著我的面吹牛钳踊,可吹牛的內(nèi)容都是我干的衷敌。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼拓瞪,長吁一口氣:“原來是場噩夢啊……” “哼缴罗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吴藻,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤瞒爬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沟堡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侧但,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年航罗,在試婚紗的時候發(fā)現(xiàn)自己被綠了禀横。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡粥血,死狀恐怖柏锄,靈堂內(nèi)的尸體忽然破棺而出酿箭,到底是詐尸還是另有隱情,我是刑警寧澤趾娃,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布缭嫡,位于F島的核電站,受9級特大地震影響抬闷,放射性物質(zhì)發(fā)生泄漏妇蛀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一笤成、第九天 我趴在偏房一處隱蔽的房頂上張望评架。 院中可真熱鬧,春花似錦炕泳、人聲如沸纵诞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浙芙。三九已至,卻和暖如春籽腕,著一層夾襖步出監(jiān)牢的瞬間茁裙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工节仿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掉蔬。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓廊宪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親女轿。 傳聞我的和親對象是個殘疾皇子箭启,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361