3-Java內(nèi)存模型

1.Java內(nèi)存模型的基礎(chǔ)

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

線程之間如何通信粹懒、線程之間如何同步

通信是指線程之間以何種機(jī)制來交換信息重付。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存消息傳遞凫乖。

共享內(nèi)存的并發(fā)模型通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信确垫。消息傳遞的并發(fā)模型必須通過發(fā)送消息來顯式進(jìn)行通信弓颈。

同步是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制。

共享內(nèi)存的并發(fā)模型同步是顯式進(jìn)行的删掀。消息傳遞的并發(fā)模型同步是隱式進(jìn)行的翔冀。

Java的并發(fā)采用的是共享內(nèi)存模型。

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

在Java中披泪,實(shí)例域纤子、靜態(tài)域、數(shù)組元素都存儲(chǔ)在堆內(nèi)存中款票,堆內(nèi)存在線程之間共享(用共享變量代指)控硼。局部變量、方法定義參數(shù)艾少、異常處理器參數(shù)不會(huì)在線程之間共享卡乾,它們不會(huì)有內(nèi)存可見性問題,也不受內(nèi)存模型的影響缚够。

Java線程之間的通信由Java內(nèi)存模型(JMM)控制幔妨。

③從源代碼到指令序列的重排序

重排序分3種類型:

1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下谍椅,可以重新安排語句執(zhí)行順序误堡。

2)指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行毯辅。如果不存在數(shù)據(jù)依賴性埂伦,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序煞额。

3)內(nèi)存系統(tǒng)的重排序思恐。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行膊毁。

1屬于編譯器重排序胀莹,2和3屬于處理器重排序。對(duì)于編譯器婚温,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序描焰。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí)栅螟,插入特定類型的內(nèi)存屏障指令荆秦,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。

JMM屬于語言級(jí)的內(nèi)存模型力图,它確保在不同的編譯器和處理器平臺(tái)上步绸,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證吃媒。

④并發(fā)編程模型的分類

由于每個(gè)處理器上寫緩沖區(qū)瓤介,僅僅對(duì)它所在的處理器可見吕喘,處理器對(duì)內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致刑桑!

下表是常見處理器允許的重排序類型的列表:N表示處理器不允許兩個(gè)操作重排序氯质,Y表示允許重排序。

Load-讀祠斧,Store-寫

為了保證內(nèi)存可見性闻察,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。JMM把內(nèi)存屏障指令分為4類琢锋,如下表

? Store Load Barriers 是一個(gè)“全能型”的屏障蜓陌,同時(shí)具有其他3個(gè)屏障的效果。現(xiàn)代的多處理器大多支持該屏障吩蔑。執(zhí)行該屏障開銷會(huì)很昂貴钮热,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)

⑤happens-before簡介

從JDK5開始,Java使用新的JSR-133內(nèi)存模型烛芬。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性隧期。JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見赘娄,那么兩個(gè)操作之間必須要存在happens-before關(guān)系仆潮。兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間遣臼。

與程序員密切相關(guān)的happens-before規(guī)則:

  • 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作性置,happens-before于該線程中的任意后續(xù)操作。
  • 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖揍堰,happens-before于隨后對(duì)這個(gè)鎖的加鎖鹏浅。
  • volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀屏歹。
  • 傳遞性:如果A happens-before B隐砸,且B happens-before C,那么A happens-before C蝙眶。

注意:

兩個(gè)操作之間具有happens-before關(guān)系季希,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見幽纷,且前一個(gè)操作按順序排在第二個(gè)操作自謙式塌。

2.重排序

重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。

①數(shù)據(jù)依賴性

如果兩個(gè)操作訪問同一個(gè)變量友浸,且這兩個(gè)操作中有一個(gè)為寫操作峰尝,這兩個(gè)操作之間就存在數(shù)據(jù)依賴性。編譯器和處理器在重排序時(shí)尾菇,不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序(只針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作境析,多個(gè)的時(shí)候不考慮數(shù)據(jù)依賴性)囚枪。

②as-if-serial語義

as-if-serial語義意思是:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變劳淆。

as-if-serial語義使單線程程序員無需擔(dān)心重排序會(huì)干擾他們链沼,也無需擔(dān)心內(nèi)存可見性問題。

③程序順序規(guī)則

根據(jù)happens-before的程序順序規(guī)則沛鸵,上面計(jì)算圓的面積的示例代碼存在3個(gè)happens-before關(guān)系括勺。

1)A happens-before B。

2)B happens-before C曲掰。

3)A happens-before C疾捍。

重排序操作A和操作B后的執(zhí)行結(jié)果與操作A和操作B按happens-before順序執(zhí)行的結(jié)果一致。這種情況下栏妖,JMM會(huì)認(rèn)為這種重排序并不非法乱豆,允許這種重排序。

④重排序?qū)Χ嗑€程的影響

class ReorderExample {
  int a = 0;
  boolean flag= false;
  public void writer() {
    a ==1;          //1
    flag == true;   //2
  }
  public void reader() {
    if(flag) {      //3
      int i = a * a; //4
      ......
    }
  }
}

3和4存在控制依賴關(guān)系吊趾。當(dāng)代碼中存在控制依賴性時(shí)宛裕,會(huì)影響指令序列執(zhí)行的并行度。為此編譯器和處理器會(huì)采用猜測執(zhí)行來克服控制相關(guān)性對(duì)并行度的影響论泛。以處理器的猜測執(zhí)行為例揩尸,執(zhí)行線程B的處理器可以提前讀取并計(jì)算a*a,然后把計(jì)算記過臨時(shí)保存到一個(gè)名為重排序緩沖的硬件緩存中屁奏。當(dāng)3的條件判斷為真時(shí)岩榆,就把該計(jì)算結(jié)果寫入變量i中。單線程程序?qū)Υ嬖诳刂埔蕾嚨牟僮髦嘏判虿粫?huì)改變執(zhí)行結(jié)果坟瓢,但在多線程程序中勇边,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果载绿。

3.順序一致性

①數(shù)據(jù)競爭與順序一致性

Java內(nèi)存模型規(guī)范對(duì)數(shù)據(jù)競爭的定義如下:

在一個(gè)線程中寫一個(gè)變量粥诫,在另一個(gè)線程讀同一個(gè)變量油航,而且寫和讀沒有通過同步來排序崭庸。

②順序一致性內(nèi)存模型

順序一致性內(nèi)存模型:是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見性保證谊囚。

順序一致性內(nèi)存模型兩大特性:

1)一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行

2)(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序怕享。每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。

兩個(gè)線程使用監(jiān)視器鎖正確同步:

兩個(gè)線程沒有做同步:整體執(zhí)行順序是無序的镰踏,但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序函筋。順序一致性內(nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見。

但是JMM中沒有這個(gè)保證奠伪。未同步程序在JMM中不但整體的執(zhí)行順序是無序的跌帐,而且所有線程看到的操作執(zhí)行順序也可能不一致首懈。(比如當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中,在沒有刷新到主內(nèi)存之前谨敛,寫操作僅對(duì)當(dāng)前線程可見究履,其他線程會(huì)認(rèn)為這個(gè)寫操作根本沒有被當(dāng)前線程執(zhí)行,這種情況下脸狸,當(dāng)前線程和其他線程看到的操作執(zhí)行順序?qū)⒉灰恢拢?/p>

③同步程序的順序一致性效果

對(duì)前面ReorderExample用鎖來同步最仑,看看正確同步的程序如何具有順序一致性。

class SynchronizedExample {
  int a = 0;
  boolean flag= false;
  public synchronized void writer() { //獲取鎖
    a ==1;          //1
    flag == true;   //2
  }                                 //釋放鎖
  public synchronized void reader() {  //獲取鎖
    if(flag) {      //3
      int i = a * a; //4
      ......
    }
  }                                 //釋放鎖
}

④未同步程序的執(zhí)行特性

對(duì)于未同步或未正確同步的多線程程序炊甲,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的值泥彤,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0卿啡,null吟吝,false)。

JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致颈娜。

未同步程序在JMM和順序一致性模型中的執(zhí)行特性有如下幾個(gè)差異:

1)順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行爸黄,而JMM不保證。

2)順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序揭鳞,而JMM不保證炕贵。

3)JMM不保證對(duì)64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性野崇。

第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)称开。

總線事務(wù):每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(wù)乓梨。

總線事務(wù)包括讀事務(wù)和寫事務(wù)鳖轰。

在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其他的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫扶镀。

總線的這些工作機(jī)制可以把所有處理器對(duì)內(nèi)存的訪問以串行化的方式來執(zhí)行蕴侣。在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器可以訪問內(nèi)存這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫操作具有原子性臭觉。

在一些32位的處理器上昆雀,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷蝠筑。JMM對(duì)64位變量的寫操作不具有原子性狞膘。

JSR-133之前的舊內(nèi)存模型中,一個(gè)64位long/double型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來執(zhí)行什乙。從JSR-133開始(JDK 1.5)挽封,僅僅只允許把64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。任意讀操作在JSR-133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)臣镣。

4.volatile的內(nèi)存語義

①volatile的特性

理解volatile特性的一個(gè)好方法是把對(duì)volatile變量的單個(gè)讀/寫辅愿,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫操作做了同步智亮。

class VolatileFeaturesExample {
  volatile long vl = 0L;        //使用volatile聲明64位的long型變量
  public void set(long l) {
    vl = l;                     //單個(gè)volatile變量的寫
  }
  public void getAndIncrement() {
    vl++;                       //復(fù)合(多個(gè))volatile變量的讀/寫
  }
  public long get() {
    return vl;                  //單個(gè)volatile變量的讀
  }
}

假設(shè)有多個(gè)線程分別調(diào)用上面程序的3個(gè)方法,這個(gè)程序在語義上和下面等價(jià)点待。

class VolatileFeaturesExample {
  long vl = 0L;                         //64位的long型普通變量
  public synchronized void set(long l) {  //對(duì)單個(gè)的普通變量的寫用同一個(gè)鎖同步
    vl = l;
  }
  public void getAndIncrement() {        //普通方法調(diào)用
    long temp = get();                  //調(diào)用已同步的讀方法
    temp += 1L;                         //普通寫操作
    set(temp);                          //調(diào)用已同步的寫方法
  }
  public synchronized long get() {        //對(duì)單個(gè)的普通變量的讀用同一個(gè)鎖同步
    return vl;
  }
}

volatile變量自身具有下列特性:

  • 可見性鸽素。對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入亦鳞。
  • 原子性馍忽。對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性燕差。

②volatile寫-讀建立的happens-before關(guān)系

從內(nèi)存語義的角度來說

class VolatileExample {
  int a = 0;
  volatile boolean flag = false;
  public void writer() {
    a ==1;          //1
    flag == true;   //2
  }
  public void reader() {
    if(flag) {      //3
      int i = a; //4
      ......
    }
  }
}

1)根據(jù)程序次序規(guī)則:1 happens-before 2遭笋;3 happens-before 4。

2)根據(jù)volatile規(guī)則:2 happens-before 3徒探。

3)根據(jù)happens-before的傳遞性規(guī)則瓦呼,1 happens-before 4。

黑色箭頭:程序順序規(guī)則测暗。

橙色箭頭:volatile規(guī)則央串。

藍(lán)色箭頭:組合這些規(guī)則后提供的happens-before保證。

③volatile寫-讀的內(nèi)存語義

volatile寫的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí)碗啄,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存质和。

volatile讀的內(nèi)存語義:當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效稚字。線程接下來將從主內(nèi)存中讀取共享變量饲宿。

volatile寫和volatile讀的內(nèi)存語義總結(jié):

  • 線程A寫一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所做修改的)消息胆描。
  • 線程B讀一個(gè)volatile變量瘫想,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)volatile變量之前對(duì)共享變量所做的修改的)消息。
  • 線程A寫一個(gè)volatile變量昌讲,隨后線程B讀這個(gè)volatile變量国夜,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。

④volatile內(nèi)存語義的實(shí)現(xiàn)

JMM針對(duì)編譯器指定的volatile重排序規(guī)則表短绸。

為了實(shí)現(xiàn)volatile的內(nèi)存語義车吹,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序鸠按。

  • 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障礼搁。
  • 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障目尖。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。

上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守扎运。在實(shí)際執(zhí)行時(shí)瑟曲,只要不改變volatile寫-讀的內(nèi)存語義饮戳,編譯器可以根據(jù)具體情況省略不必要的屏障。

class VolatileBarrierExample {
  int a;
  volatile int v1 = 1;
  volatile int v2= 2;
  void readAndWrite() {
    int i = v1;     //第一個(gè)volatile讀
    int j = v2;     //第二個(gè)volatile讀
    a = i + j;      //普通寫
    v1 = i + 1;     //第一個(gè)volatile寫
    v2 = j + 2;     //第二個(gè)volatile寫
  }
  ......        //其他方法
}

最后的StoreLoad屏障不能省略洞拨。編譯器可能無法精確斷定第二個(gè)寫之后是否會(huì)有volatile讀或?qū)憽?/p>

由于X86處理器僅會(huì)對(duì)寫-讀操作重排序扯罐,所以在X86中,JMM僅需在volatile寫后面插入一個(gè)StoreLoad屏障即可正確實(shí)現(xiàn)volatile寫-讀的內(nèi)存語義烦衣。

⑤JSR-133為什么要增強(qiáng)volatile的內(nèi)存語義

在JSR-133之前的舊Java內(nèi)存模型中歹河,雖然不允許volatile變量之間重排序,但舊的Java內(nèi)存模型允許volatile變量與普通變量重排序花吟。在舊的內(nèi)存模型中秸歧,VolatileExample示例程序可能被重排序成下列時(shí)序來執(zhí)行:

為了提供一種比鎖更輕量級(jí)的線程之間通信的機(jī)制,JSR-133專家組決定增強(qiáng)volatile的內(nèi)存語義衅澈。如果想在程序中用volatile代替鎖键菱,請(qǐng)一定謹(jǐn)慎,具體詳情參閱Brian Goetz的文章《Java理論與實(shí)踐:正確使用Volatile變量》今布。

5.鎖的內(nèi)存語義

①鎖的釋放-獲取 建立的happens-before關(guān)系

class MonitorExample {
  int a = 0;
  public synchronized void writer() {   //1
    a++;                            //2
  }                                 //3
  public synchronized void reader() {   //4
    int i = a;                      //5
    ......
  }                                 //6
}

線程A執(zhí)行writer()方法经备,線程B執(zhí)行reader()方法,根據(jù)happens-before規(guī)則部默,這個(gè)過程包含的happens-before關(guān)系:

1)根據(jù)程序次序規(guī)則:1 happens-before 2侵蒙,2 happens-before 3,4 happens-before 5傅蹂,5 happens-before 6蘑志。

2)根據(jù)監(jiān)視器鎖規(guī)則:3 happens-before 4。

3)根據(jù)happens-before的傳遞性:2 happens-before 5贬派。

黑色箭頭:程序順序規(guī)則急但。

橙色箭頭:監(jiān)視器鎖規(guī)則。

藍(lán)色箭頭:組合這些規(guī)則后提供的happens-before保證搞乏。

②鎖的釋放和獲取的內(nèi)存語義

當(dāng)線程釋放鎖時(shí)波桩,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。

當(dāng)線程獲取鎖時(shí)请敦,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效镐躲。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。

以MonitorExample為例:

A線程釋放鎖后侍筛,共享數(shù)據(jù)的狀態(tài)示意圖:

鎖獲取的狀態(tài)示意圖:

鎖釋放(對(duì)應(yīng)volatile寫)和鎖獲扔┰怼(對(duì)應(yīng)volatile讀)的內(nèi)存語義總結(jié):

  • 線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(其對(duì)共享變量所做修改的)消息匣椰。
  • 線程B獲取一個(gè)鎖裆熙,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做的修改的)消息。
  • 線程A釋放鎖,隨后線程B獲取這個(gè)鎖入录,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息蛤奥。

③鎖內(nèi)存語義的實(shí)現(xiàn)

class ReentrantLockExample {
  int a = 0;
  ReentrantLock lock = new ReentrantLock();
  public void writer() {
    lock.lock();        //獲取鎖
    try {
      a++;
    } finally {
      lock.unlock();        //釋放鎖
    }
  }
  public void reader() {
    lock.lock();        //獲取鎖
    try {
      int i = a;
      ......
    } finally {
      lock.unlock();        //釋放鎖
    }
  }
}

ReentrantLock的實(shí)現(xiàn)依賴于Java同步器框架AbstractQueuedSynchronizer(本文簡稱為AQS)。AQS使用一個(gè)整型的volatile變量state來維護(hù)同步狀態(tài)僚稿。

ReentrantLock分為公平鎖和非公平鎖凡桥。

公平鎖,lock()調(diào)用軌跡:

1)ReentrantLock:lock()蚀同。

2)FairSync:lock()缅刽。

3)AbstractQueuedSynchronizer:acquire(int arg)。

4)ReentrantLock.FairSync:tryAcquire(int acquires)蠢络。

第4步真正開始加鎖衰猛,下面是該方法源代碼

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); //獲取鎖的開始焦读,首先讀volatile變量state
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平鎖庇勃,unlock()調(diào)用軌跡:

1)ReentrantLock:unlock()晰骑。

2)AbstractQueuedSynchronizer:release(int arg)瞭空。

3)ReentrantReadWriteLock.Sync:tryRelease(int releases)症革。

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);//釋放鎖的最后区岗,寫volatile變量state
    return free;
}

從上面源碼中谨娜,可以看出加鎖方法先讀volatile變量state洼哎。在鎖釋放的最后寫volatile變量state酸茴。根據(jù)volatile的happens-before規(guī)則分预,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個(gè)volatile變量后將立即變得對(duì)獲取鎖的線程可見薪捍。

非公平鎖笼痹,lock()調(diào)用軌跡:

1)ReentrantLock:lock()。

2)ReentrantLock.NonfairSync:lock()酪穿。

3)AbstractQueuedSynchronizer:compareAndSetState(int expect, int update)凳干。

第3步真正開始加鎖,下面是該方法源代碼:

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

非公平鎖被济,unlock()調(diào)用軌跡:同公平鎖

公平鎖和非公平鎖的內(nèi)存語義總結(jié):

  • 公平鎖和非公平鎖釋放時(shí)救赐,最后都要寫一個(gè)volatile變量state。
  • 公平鎖獲取時(shí)只磷,首先會(huì)讀volatile變量经磅。
  • 非公平鎖獲取時(shí),首先會(huì)用CAS更新volatile變量钮追,這個(gè)操作同時(shí)具有volatile讀和volatile寫的內(nèi)存語義预厌。

鎖釋放-獲取的內(nèi)存語義的實(shí)現(xiàn)至少有兩種方式:

1)利用volatile變量的寫-讀所具有的內(nèi)存語義。

2)利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義元媚。

④concurrent包的實(shí)現(xiàn)

Java線程之間的通信有4種方式:

1)A線程寫volatile變量轧叽,隨后B線程讀這個(gè)volatile變量苗沧。

2)A線程寫volatile變量,隨后B線程用CAS更新這個(gè)volatile變量犹芹。

3)A線程用CAS更新一個(gè)volatile變量崎页,隨后B線程用CAS更新這個(gè)volatile變量鞠绰。

4)A線程用CAS更新一個(gè)volatile變量腰埂,隨后B線程讀這個(gè)volatile變量。

分析concurrent包的源代碼實(shí)現(xiàn)蜈膨,會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:

  • 聲明共享變量為volatile
  • 使用CAS的原子條件更新來實(shí)現(xiàn)線程之間的同步屿笼。
  • 配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實(shí)現(xiàn)線程之間的通信。

6.final域的內(nèi)存語義

①final域的重排序規(guī)則

對(duì)于final域翁巍,編譯器和處理器要遵守兩個(gè)重排序規(guī)則:

  • 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入驴一,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序灶壶。
  • 初次讀一個(gè)包含final域的對(duì)象的引用肝断,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序驰凛。
public class FinalExample {
  int i;                    //普通變量
  final int j;              //final變量
  static FinalExample obj;
  public FinalExample () {      //構(gòu)造函數(shù)
    i = 1;                  //寫普通域
    j = 2;                  //寫final域
  }
  public static void writer () {    //寫線程A執(zhí)行
    obj = new FinalExample ();
  }
  public static void reader () {    //讀線程B執(zhí)行
    FinalExample object = obj;      //讀對(duì)象引用
    int a = object.i;           //讀普通域
    int b = object.j;           //讀final域
  }
}

②寫final域的重排序規(guī)則

寫final域的重排序規(guī)則禁止把final域的寫重排序到構(gòu)造函數(shù)之外胸懈。規(guī)則實(shí)現(xiàn)包括:

  • JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外。
  • 編譯器會(huì)在final域的寫之后恰响,構(gòu)造函數(shù)return之前趣钱,插入一個(gè)StoreStore屏障。這個(gè)屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外胚宦。

寫final域的重排序規(guī)則可以確保:在對(duì)象引用為任意線程可見之前首有,對(duì)象的final域已經(jīng)被正確的初始化過了,而普通域不具有這個(gè)保障枢劝。

③讀final域的重排序規(guī)則

讀final域的重排序規(guī)則:在一個(gè)線程中井联,初次讀對(duì)象引用與初次讀該對(duì)象包含的final域,JMM禁止處理器重排序這兩個(gè)操作您旁。編譯器會(huì)在讀final域操作的前面插入一個(gè)LoadLoad屏障烙常。

讀final域的重排序規(guī)則可以確保:在讀一個(gè)對(duì)象的final域之前,一定會(huì)先讀包含這個(gè)final域的對(duì)象的引用被冒,而普通域不具有這個(gè)保障军掂。

④final域?yàn)橐妙愋?/h4>

引用類型示例代碼:

public class FinalReferenceExample {
  final int[] intArray;             //final是引用類型
  static FinalReferenceExample obj;
  public FinalReferenceExample () {  //構(gòu)造函數(shù)
    intArray = new int[1];          //1
    intArray[0] = 1;                //2
  }
  public static void writerfOne () {  //寫線程A執(zhí)行
    obj = new FinalReferenceExample ();//3
  }
  public static void writerTwo () {  //寫線程B執(zhí)行
    obj.intArray[0] = 2;            //4
  }
  public static void reader () {    //讀線程C執(zhí)行
    if (obj != null) {              //5
      int temp1 = obj.intArray[0];   //6
    }
  }
}

對(duì)于引用類型,寫final域的重排序規(guī)則對(duì)編譯器和處理器增加了如下約束:

在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫入昨悼,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量蝗锥,這兩個(gè)操作之間不能重排序。(2和3不能重排序)

線程C不一定能看到線程B的操作率触。如果要確保能看到终议,寫線程B和讀線程C之間需要使用同步原語(lock或volatile)來確保內(nèi)存可見性。

⑤為什么final引用不能從構(gòu)造函數(shù)內(nèi)“溢出”

public class FinalReferenceEscapeExample {
  final int i;
  static FinalReferenceEscapeExample obj;
  public FinalReferenceEscapeExample () {
    i = 1;              //1寫final域
    obj = this;         //2 this引用在此“逸出”
  }
  public static void writer () {
    new FinalReferenceEscapeExample ();
  }
  public static void reader () {
    if (obj != null) {      //3
      int temp = obj.i;     //4
    }
  }
}

從圖可以看出:在構(gòu)造函數(shù)返回前,被構(gòu)造對(duì)象的引用不能為其他線程所見穴张,因?yàn)榇藭r(shí)的final域可能還沒有被初始化细燎。在構(gòu)造函數(shù)返回后,任意線程都將保證能看到final域正確初始化之后的值皂甘。

我的理解:不能在構(gòu)造器中讓obj=this玻驻,這樣會(huì)造成this還沒有構(gòu)造完,引用就溢出了

⑥final語義在處理器中的實(shí)現(xiàn)

X86處理器中偿枕,final域的讀/寫不會(huì)插入任何內(nèi)存屏障璧瞬!

⑦JSR-133為什么要增強(qiáng)final的語義

在舊的Java內(nèi)存模型中,一個(gè)最嚴(yán)重的缺陷是線程可能看到final域的值會(huì)改變(先看到未初始化的默認(rèn)值渐夸,再看到初始化之后的值)嗤锉。最常見的例子是在舊的Java內(nèi)存模型中,String的值可能會(huì)改變墓塌。

JSR-133專家組增強(qiáng)了final的語義瘟忱,通過為final域增加寫和讀重排序規(guī)則,可以為Java程序員提供初始化安全保證:只要對(duì)象是正確構(gòu)造的(被構(gòu)造對(duì)象的引用在構(gòu)造函數(shù)中沒有"逸出")苫幢,那么不需要使用同步(lock和volatile的使用)就可以保證任意線程都能看到這個(gè)final域在構(gòu)造函數(shù)中被初始化之后的值访诱。

7.happens-before

①JMM的設(shè)計(jì)

double pi = 3.14;   //A
double r = 1.0; //B
double area = pi * r * r;   //C

A happens-before B, B happens-before C态坦, A happens-before C盐数。(1不必要,2和3是必需的)

JMM把happens-before要求禁止的重排序分為兩類:

  • 會(huì)改變程序執(zhí)行結(jié)果的重排序伞梯。JMM要求編譯器和處理器必須禁止這種重排序玫氢。
  • 不會(huì)改變程序執(zhí)行結(jié)果的重排序。JMM允許這種重排序谜诫。

JMM遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程和正確同步的多線程程序)漾峡,編譯器和處理器怎么優(yōu)化都行。例如如果編譯器經(jīng)過細(xì)致的分析后喻旷,認(rèn)定一個(gè)鎖/volatile只會(huì)被單個(gè)線程訪問生逸,那么這個(gè)鎖/volatile可消除。

②happens-before的定義

happens-before關(guān)系的定義如下:(單線程和正確同步的多線程程序)

  • 如果一個(gè)操作happens-before另一個(gè)操作且预,那么第一個(gè)操作的執(zhí)行結(jié)果對(duì)第二個(gè)操作可見槽袄。這是JMM對(duì)程序員的承諾。
  • 兩個(gè)操作之間存在happens-before關(guān)系锋谐,如果重排序之后的執(zhí)行結(jié)果與原來結(jié)果一致遍尺,那么這種重排序并不非法。這是JMM對(duì)編譯器和處理器重排序的約束原則涮拗。

happens-before與as-if-serial類似:

  • 執(zhí)行結(jié)果不被改變:as-if-serial保證單線程內(nèi)程序的乾戏,happens-before關(guān)系保證正確同步的多線程程序的迂苛。
  • 給程序員創(chuàng)造一個(gè)幻境:as-if-serial語義-單線程程序按程序的順序來執(zhí)行,happens-before關(guān)系-正確同步的多線程是按happens-before指定的順序來執(zhí)行的鼓择。

③happens-before規(guī)則

happens-before規(guī)則:

  • 程序順序規(guī)則:UI個(gè)線程中的每個(gè)操作三幻,happens-before于該線程中的任意后續(xù)操作。
  • 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖呐能,happens-before于隨后對(duì)這個(gè)鎖的加鎖念搬。
  • volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀催跪。
  • 傳遞性:如果A happens-before B锁蠕, 且B happens-before C夷野,那么A happens-before C懊蒸。
  • start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作悯搔。
  • join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回骑丸,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。

8.雙重檢查鎖定與延遲初始化

①雙重檢查鎖定的由來

在Java多線程程序中妒貌,有時(shí)候需要采用延遲初始化來降低初始化類和創(chuàng)建對(duì)象的開銷通危。雙重檢查鎖定是常見的延遲初始化技術(shù),但它是一個(gè)錯(cuò)誤的用法灌曙。

public class DoubleCheckedLocking {                 //1
  private static Instance instance;                 //2
  public static Instance getInstance() {            //3
    if (instance == null) {                         //4:第一次檢查
      synchronized (DoubleCheckedLocking.class) {   //5:加鎖
        if (instance == null) {                     //6:第二次檢查
          instance = new Instance();                //7:問題的根源出在這
        }                                           //8
      }                                             //9
      return instance;                              //10
    }                                               //11
  }
}

②問題的根源

第7行代碼可以分解為如下偽代碼:

memory = allocate()菊碟;//1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory);//2:初始化對(duì)象
instance = memory在刺;//3:設(shè)置instance指向剛分配的內(nèi)存地址

上面?zhèn)未a中的2和3之間逆害,可能會(huì)被重排序:

memory = allocate();//1:分配對(duì)象的內(nèi)存空間
instance = memory蚣驼;//3:設(shè)置instance指向剛分配的內(nèi)存地址
                    //注意魄幕,此時(shí)對(duì)象還沒有被初始化!
ctorInstance(memory)颖杏;//2:初始化對(duì)象

解決方案:

  • 不允許2和3重排序纯陨。
  • 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序留储。

③基于volatile的解決方案

需要JDK5或更高版本翼抠。

public class DoubleCheckedLocking {                 //1
  private volatile static Instance instance;                    //2
  public static Instance getInstance() {            //3
    if (instance == null) {                         //4:第一次檢查
      synchronized (DoubleCheckedLocking.class) {   //5:加鎖
        if (instance == null) {                     //6:第二次檢查
          instance = new Instance();                //7:instance為volatile,現(xiàn)在沒有問題了
        }                                           //8
      }                                             //9
      return instance;                              //10
    }                                               //11
  }
}

④基于類初始化的解決方案

public class InstanceFactory {
  private static class InstanceHolder {
    public static Instance instance = new Instance();
  }
  public static Instance getInstance() {
    return InstanceHolder.instance;//這里將導(dǎo)致InstanceHolder類被初始化
  }
}

初始化類的情況:

  • T是一個(gè)類获讳,且一個(gè)T類型的實(shí)例被創(chuàng)建阴颖。
  • T是一個(gè)類,且T中聲明的一個(gè)靜態(tài)方法被調(diào)用赔嚎。
  • T中聲明的一個(gè)靜態(tài)字段被賦值膘盖。
  • T中聲明的一個(gè)靜態(tài)字段被使用胧弛,而且這個(gè)字段不是一個(gè)常量字段。(InstanceFactory符合該情況)
  • T是一個(gè)頂級(jí)父類侠畔,而且一個(gè)斷言語句嵌套在T內(nèi)部被執(zhí)行结缚。

Java語言規(guī)范規(guī)定,對(duì)于每一個(gè)類或接口软棺,都有一個(gè)唯一的初始化鎖與之對(duì)應(yīng)红竭。

類初始化的處理過程:虛構(gòu)出condition和state標(biāo)記,方便理解喘落。

  • 第1階段:通過在Class對(duì)象上同步(即獲取Class對(duì)象的初始化鎖)茵宪,來控制類或接口的初始化。
  • 第2階段:線程A執(zhí)行類的初始化瘦棋,同事線程B在初始化鎖對(duì)應(yīng)的condition上等待稀火。
  • 第3階段:線程A設(shè)置state=initialized,然后喚醒在condition中等待的所有線程赌朋。
  • 第4階段:線程B結(jié)束類的初始化處理凰狞。
  • 第5階段:線程C執(zhí)行類的初始化處理。

線程B在第4階段的B1獲取同一個(gè)初始化鎖沛慢,根據(jù)happens-before關(guān)系赡若,保證:線程A執(zhí)行類的初始化時(shí)的寫入操作(執(zhí)行類的靜態(tài)初始化和初始化類中聲明的靜態(tài)字段),線程B一定能看到团甲。

9.Java內(nèi)存模型綜述

①處理器的內(nèi)存模型

內(nèi)存模型類型:

  • 放松程序中寫-讀操作的順序逾冬,由此產(chǎn)生了Total Store Ordering內(nèi)存模型。簡稱TSO躺苦。
  • 在TSO基礎(chǔ)上身腻,繼續(xù)放松程序中寫-寫操作的順序,由此產(chǎn)生了Partial Store Order內(nèi)存模型圾另。簡稱PSO霸株。
  • 在TSO和PSO基礎(chǔ)上,繼續(xù)放松程序中讀-寫和讀-讀操作的順序集乔,由此產(chǎn)生了Relaxed Memory Order內(nèi)存模型(檢查RMO)和PowerPC內(nèi)存模型去件。

②各種內(nèi)存模型之間的關(guān)系

③JMM的內(nèi)存可見性保證

④JSR-133對(duì)舊內(nèi)存模型的修補(bǔ)

主要修補(bǔ)有兩個(gè):

  • 增強(qiáng)volatile的內(nèi)存語義。舊內(nèi)存模型允許volatile變量與普通變量重排序扰路。
  • 增強(qiáng)final的內(nèi)存語義尤溜。舊內(nèi)存模型多次讀取同一個(gè)final變量的值可能會(huì)不相同。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汗唱,一起剝皮案震驚了整個(gè)濱河市宫莱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哩罪,老刑警劉巖授霸,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巡验,死亡現(xiàn)場離奇詭異,居然都是意外死亡碘耳,警方通過查閱死者的電腦和手機(jī)显设,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辛辨,“玉大人捕捂,你說我怎么就攤上這事《犯悖” “怎么了指攒?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長僻焚。 經(jīng)常有香客問我允悦,道長,這世上最難降的妖魔是什么溅呢? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任澡屡,我火速辦了婚禮,結(jié)果婚禮上咐旧,老公的妹妹穿的比我還像新娘。我一直安慰自己绩蜻,他們只是感情好铣墨,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著办绝,像睡著了一般伊约。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孕蝉,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天屡律,我揣著相機(jī)與錄音,去河邊找鬼降淮。 笑死超埋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的佳鳖。 我是一名探鬼主播霍殴,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼系吩!你這毒婦竟也來了来庭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤穿挨,失蹤者是張志新(化名)和其女友劉穎月弛,沒想到半個(gè)月后肴盏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡帽衙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年叁鉴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佛寿。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡幌墓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冀泻,到底是詐尸還是另有隱情常侣,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布弹渔,位于F島的核電站胳施,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏肢专。R本人自食惡果不足惜舞肆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望博杖。 院中可真熱鬧椿胯,春花似錦、人聲如沸剃根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狈醉。三九已至廉油,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苗傅,已是汗流浹背抒线。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留渣慕,地道東北人嘶炭。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像摇庙,于是被迫代替她去往敵國和親旱物。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容