一篇文章看懂java併發編程

Hi,朋友們,你們很久不見。這兩個月來發生了不少的事情,疫情爆發,不知道有多少的家庭深受其害,瀕臨破碎,也不知道有多少中小企業面臨着復工難,無力發放工資的困局。在此國難之際,咱們更應該信任咱們的國家,積極配合工做,祈禱疫情早日結束,人民生活早日迴歸正軌。武漢加油,中國加油!
java

不過咱們的學習仍是要繼續。在家不能外出的日子,看些文章深化一下知識也是個不錯的選擇呢。今天我想分享的主題是java併發編程。以前對併發編程有一些瞭解,可是感受就沒有造成體系。因此這段時間我又從新學習了一下併發編程,但願能把本身所學東西天然的造成體系並分享出來。本文較長,建議電腦上觀看。程序員

下面是java併發編程系列的大綱圖: 編程



一.併發問題的源頭

Java併發編程一直算是比較進階的知識,相信不少的java程序員也曾經學習過一些零碎的知識,也都知道諸如volatile是輕量版的鎖,會用synchronize來保證代碼的同步。可是可能也有一些朋友並不知道該在什麼場景下去使用對應的工具包,也就是不知道每種工具究竟是解決了什麼問題。咱們先來聊聊爲何併發程序可能會致使各類詭異的bug。segmentfault

隨着技術的發展,CPU,內存和IO設備都有了巨大的進步。CPU核數從單核變化到多核,同一時刻CPU可能執行着不少個任務,或者一個任務也可能同時被多個CPU在運行着。內存和IO設備的速度和容量也突飛猛進。不過再怎麼變化,有一個核心矛盾即三者的速度差別都一直存在着,而且差距之大如同天上一日,人間十年。咱們的程序大部分都須要訪問內存,有些還要訪問IO,這樣的話,總體性能都會被訪問內存和IO的速度給限制着,CPU可能大部分時候都在苦巴巴的空等。數組

爲了平衡三者的速度差別,提升CPU的利用率,計算機結構,操做系統,編譯器都作出了一些優化,主要是:緩存

  1. CPU 增長緩存,把最近用到的數據放入緩存裏,要比直接讀取內存數據要快不少。
  2. 操做系統 增長進程和線程的概念,以便分時複用CPU,均衡CPU與IO設備的速度差別
  3. 編譯程序 優化指令執行次序,使得上面提到的緩存中的數據能夠更合理的利用

不過正是由於這些優化項,咱們在寫併發程序的時候才容易出那麼多詭異的bug。bash

可見性問題

上面咱們說到,CPU會增長緩存,來均衡CPU和內存的速度差別。在單核時候,全部線程都是在一個CPU上面運行,這個優化並不會帶來問題。由於只有一個緩存,線程A在緩存中的寫,等到CPU運行線程B的時候,線程B必定能看到這個寫以後的結果。多線程

一個線程對共享變量的修改,另外一個線程能馬上看到,咱們就稱爲可見性併發

可是到了多核時代,多個CPU,多份緩存,線程A在CPU A的緩存中的寫操做,線程B在CPU B中卻不必定能看到,由於他們是操做的不一樣的緩存,這個時候線程A的寫操做對B而言就不具備可見性了。下面用個例子展現一下。app

public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 建立兩個線程,執行add()操做
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 啓動兩個線程
    th1.start();
    th2.start();
    // 等待兩個線程執行結束
    th1.join();
    th2.join();
    return count;
  }
}
複製代碼

咱們用了兩個線程在同一個線程裏面分別把count加一了10000次,要是在單核CPU上面,結果就應該是20000,不過在多核CPU上面,結果可能就是個隨機數,爲何呢?假設線程A和線程B同時開始運行,那麼開始都是0,而後都增長1之後都同時寫入內存,這時候內存中就是1而不是咱們指望的2了。由於這樣讀取和寫入內存都不肯定,最終的結果可能就是個隨機數。

這就是多個緩存帶來的可見性問題,一個線程都數據的修改不能及時被別的線程看到。

原子性問題

爲了提升CPU的使用效率,操做系統發明了多進程和線程。操做系統容許某個線程執行一個時間片的時間,而後就會從新選擇一個線程來執行。這樣在等待IO的時候,操做系統就能夠把時間片讓給別的線程,讓別的線程去執行,提升CPU利用率。

這個設計看起來很是的天然,可是由於任務切換的時機大多數是在時間片結束的時候,可是現代的高級語言一條語句每每須要多條CPU指令,好比咱們熟知的 count += 1 這個操做,就須要三條指令:

  • 把變量count從內存加載到CPU的寄存器
  • 在寄存器中執行 +1 操做
  • 把結果寫回內存(緩存)。

時間片的切換,可能發生在任何一個CPU指令執行完的時候,這樣兩個線程切換的時候可能會致使一些奇怪的問題。這樣就比較坑啦。好比這張圖顯示的,兩個線程都執行了+1的操做,最終獲得的結果不是咱們指望的2,而是1.


咱們把一個或多個操做在CPU執行的過程當中不被中斷的特性稱爲原子性。CPU只能保證指令級別的原子操做,而不是高級語言的操做符。因此咱們須要經過別的方法保證操做的原子性。

有序性問題

編譯器爲了優化性能,有時候會改變程序中語句的執行順序。例如程序中:「a=6;b=7;」,編譯器優化後可能變成「b=7;a=6;」。在這個例子中,編譯器的調整並無影響程序的最終成果。可是有些時候也會產生意想不到的bug。

java領域的一個比較經典的案例就是雙重檢查的單例模式,好比下面的代碼。

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}
複製代碼

上面的代碼乍看之下好像沒啥問題,可能有些同窗看了上面原子性問題的部分會有所想法,會不會是這裏Singleton的初始化語句的問題呢?沒錯,問題確實出在這裏。由於new操做並非個原子性操做。它實際上包含了三步:

  1. 分配一塊內存 M;
  2. 在內存 M 上初始化 Singleton 對象;
  3. 而後 M 的地址賦值給 instance 變量

操做系統可能會優化爲1-->3-->2的操做順序。這樣的話,假設線程 A 先執行 getInstance() 方法,當執行完指令 2 時剛好發生了線程切換,切換到了線程 B 上;若是此時線程 B 也執行 getInstance() 方法,那麼線程 B 在執行第一個判斷時會發現 instance != null ,因此直接返回 instance,而此時的 instance 是沒有初始化過的,若是咱們這個時候訪問 instance 的成員變量就可能觸發空指針異常。


解決辦法是啥呢?加volatile修飾變量,或者改成

小結

併發程序一般會出現各類詭異問題,可能乍看之下很是的無厘頭,不知從何查起,可是隻要咱們深入的理解了可見性,原子性,有序性,咱們就能知道在某些場景下可能會出現什麼問題,而且知道java提供的併發工具各自都是在解決什麼問題。後面的併發理論基礎,咱們會介紹Java是如何解決這些問題的。

二.併發理論基礎

上面咱們講到了併發程序常見的可見性,原子性和有序性問題,接下來咱們就介紹java爲了解決這些問題都作了什麼努力。我將其分爲java內存模型(JMM, Java Memory Model)和併發編程基礎(主要是線程相關的併發知識)。這兩項都是至關重要的併發編程背景知識,能夠幫助咱們更好的理解和編碼。

Java內存模型(JMM)

在併發編程中,須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步。這裏的同步指的是程序中用於控制不一樣線程間操做發生相對順序的機制。常見的線程的通訊機制有兩種,共享內存消息傳遞。這兩種機制在兩個關鍵問題的處理上有一些區別:

1.共享內存:在共享內存的併發模型中,線程之間共享程序的公共狀態,經過寫-讀內存中的公共狀態進行隱式通訊。之因此是說隱式通訊,是由於兩個線程沒有直接聯繫,可是經過共享內存又拿到了對方的相關結果。可是同步是顯式的,程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。好比下圖


2.消息傳遞:在消息傳遞的併發模型中,線程之間沒有公共狀態,線程之間須要互相發送消息,因此通訊是顯式的。同時因爲發送消息和接收消息有前後順序,因此兩個線程之間相對順序就已經隱式的指定了。好比下圖


Java使用的是共享內存模型。程序員須要瞭解隱式通訊的工做機制,不然就可能遇到各類奇怪的內存可見性問題。

JMM基礎

1.抽象結構
java線程之間的通訊由JMM控制,JMM決定了一個線程對共享變量的寫入什麼時候對另外一個線程可見。同時它定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,存儲着該線程已讀/寫共享變量的副本。固然這裏的本地內存是抽象概念,包括了上面講到的緩存,寄存器等等。表明着線程存儲本數據的地方。

注意:這裏提到的共享變量指的是實例域,靜態域和數組元素之類的存儲於堆內存中的變量。只有堆內存才能被線程之間共享。不太清楚的能夠參考JVM的堆和棧

Java內存模型的抽象示意如圖所示。正如咱們前面所講,主存中存儲着共享數據,每一個線程中的私有內存存儲着共享變量的副本。


2.重排序
第一節中,咱們也講到了爲了提升性能,編譯器和處理器常常會對指令作重排序。重排序分爲三種:

  • 編譯器優化重排序。即編譯器在不改變as-if-serial語義的前提下,能夠從新安排語句的執行順序。可能你們不太as-if-serial語義是啥意思,後面會再講到。
  • 指令級並行重排序。若是不存在數據依賴性,處理器能夠改變語句對應的機器指令的執行順序
  • 內存系統重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得讀寫操做看上去多是在亂序執行。

上面三種,第一種屬於編譯器重排序,2和3屬於處理器重排序。爲了解決重排序帶來的可見性和有序性問題,JMM會禁止特定類型的編譯器重排序,而且經過插入內存屏障的方式來精緻特定類型的處理器重排序。內存屏障咱們會在後面講到。

這裏朋友們可能就有疑問了,編譯器和處理器會在什麼狀況下禁止重排序呢,有沒有什麼判斷依據?這裏主要的依據是兩個操做間有沒有數據依賴性

數據依賴性:若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,那麼這兩個操做間就存在數據依賴性。這麼一看是否是很好判別,只要有寫操做就能夠認爲是有數據依賴性了。好比寫後讀(a=1, b=a),寫後寫(a=1, a=2),讀後寫(a=b, b=1)。這三種類型只要有了重排序,執行結果就會被改變,編譯器和處理器會禁止有數據依賴性的兩個操做的執行順序。

介紹到這裏,咱們就能夠引出as-if-serial語義了。as-if-serial語義指的是無論編譯器和處理器怎麼重排序,單線程程序的執行結果不能被改變。編譯器和處理器都必須遵照as-if-serial語義。這是咱們的程序能穩定且符合預期運行的保障。舉個栗子,假設咱們有個程序

a = 1
b = 2
c = a * b
複製代碼

那麼a和b能夠重排序,可是a和c,b和c就不能重排序,由於他們有數據依賴性

3.總線工做機制
上面咱們講到了,緩存會致使可見性的問題是由於緩存刷入內存不及時,那麼問題來了,若是很及時,兩個線程同時寫入緩存,而後兩個緩存同時寫入內存,這樣內存中的數據會衝突嗎?答案是不會,這與總線工做機制密切相關。

在計算機中,數據經過總線在處理器和內存之間傳遞,每次處理器和內存之間的數據傳遞都是經過一系列步驟完成的,稱之爲總線事務。總線事務包括讀事務和寫事務。讀事務從內存傳送數據處處理器,寫事務從處理器傳送數據到內存。而且,總線會同步試圖併發使用總線的事務。也就是說同一時刻只有一個處理器在執行總線事務。其他的處理器須要等待前一個處理器完成事務後才能執行操做。


4.總結
本小節咱們講到了JMM的抽象結構,讓你們看到了緩存是怎麼樣影響到數據同步的,同時也介紹了重排序的類型和判別重排序的依據-數據依賴性,最後介紹了總線的工做機制,旨在於讓你們造成對JMM工做原理的初步認識,能認識到這些機制是保障程序穩定運行的前提。

Happens-before規則

Happens-before是JMM最核心的概念,做爲Java程序員,理解了Happens-before規則,就理解了JMM的關鍵,至關於打通了任督二脈。對於這一塊,你們務必提起幹勁。

1.設計思路
JMM設計之初,設計者們須要考慮到兩方的重要需求:

  • 程序員對內存模型的使用:但願易於理解,易於編程,不須要了解到方方面面,可是又要穩定可靠
  • 編譯器和處理器對內存模型的實現:但願約束儘量少,這樣它們能夠作儘可能多的優化

設計者們爲了平衡二者的需求,想了個好辦法,對上層的程序員,提供一套happens-before規則,程序員基於這套規則提供的內存可見性保障來編程。而對下層的編譯器等,要禁止掉會改變程序執行結果的重排序。除此以外,編譯器等想怎麼優化都行,好比把沒用的加鎖和volatile變量處理給去掉等。


2.定義
happens-before概念用於指定兩個操做之間的執行順序,這兩個操做能夠是一個線程的,也能夠是不一樣線程之中。它的定義是:若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見。好比操做A讀寫了a和b變量,同時操做A是happens before操做B的,那麼在操做B執行的時候,它是能看到操做A完成之後的a和b變量的結果的。

下面咱們仍是用以前的例子。

a = 1        // 操做1
b = 2        // 操做2
c = a * b    // 操做3
複製代碼

對於這個例子, 操做1 happens-before 操做2 操做1 happens-before 操做3 操做2 happens-before 操做3

可是要注意的是,happens-before規則不必定對應程序執行順序,這也是設計者們對於編譯器和處理器的「放水」。就數據依賴性而言,3個happens-before關係中,2和3是必須的,可是1不是必要的。因此操做1和操做2的執行順序是能夠顛倒的。編譯器和處理器在這種狀況下就會盡量的進行優化。

3.happens-before規則
java中一共定義了六條happens-before規則:

  1. 程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
  2. 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖
  3. volatile變量規則:對一個volatile變量的寫,happens-before於任意後續對這個volatile變量的讀
  4. 傳遞性:若是A happens-before B,B happens-before C,那麼A happens-before C。
  5. start()規則:若是線程A執行操做ThreadB.start(), 那麼A線程的ThreadB.start操做happens-before於線程B中的任意操做
  6. join()規則:若是線程A執行操做ThreadB.join()併成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。

因爲編譯器和處理器都必須知足as-if-serial語義,as-if-serial語義進一步保證了程序順序規則。所以程序順序規則至關於咱們前面提到的as-if-serial語義的封裝。 這裏的start和join規則主要是提供了線程切換時候的可見性保證,而前面四條規則提供了咱們平常使用到的各類工具的可見性保證。

happens-before很是重要,對於後面咱們理解鎖和工具集的實現原理十分關鍵。你們先記着,等後面會常常性的用到。

volatile內存語義

大多數java學習者在一開始學習volatile的時候,都是記得:volatile能夠看作是輕量級的鎖,其修飾的變量的變動可以保證多線程的可見性。我剛學volatile的時候,覺得volatile只是java提供的一個小小的工具,可是看到java的happens-before規則中專門有volatile的一項,就感受真的不是那麼簡單。

咱們能夠把對volatile變量的單個讀/寫,當作是使用同一個鎖對這些單個讀/寫操做作了同步。因此volatile變量具備下列特性:

  • 可見性。對volatile變量的讀,老是能看到任何線程對這個變量最後的寫入。
  • 原子性。對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。

介紹完了volatile變量的特性,咱們結合上面提到的JMM內存抽象結構介紹下JMM對於volatile變量讀寫的操做。當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中,讀的時候,volatile變量強制從主內存中讀取。這樣就至關於禁用了CPU緩存。這裏須要注意到的是JMM不止會把volatile變量刷新到主內存,而是把本地內存中的全部共享變量。這個特性被用到了java concurrent包的各類工具類中。下面用個小栗子來講明。

public class VolatileExample {
    int x = 0;
    volatile boolean v = false;
    public void writer() {
        x = 42;
        v = true;
    }
    public void reader() {
        if (v == true) {
            System.out.println("x = " + x);
        }
    }
}
複製代碼

好比這個類,一個線程執行writer,而後另外一個線程再執行reader,就會輸出x=42,這就是由於共享變量在volatile變量寫的時候都被刷入進去主內存了。注意,volatile變量必定要最後寫,最早讀。

接下來能夠總結volatile寫和volatile讀的內存語義:

  • 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出來(其對共享變量所作修改的)消息
  • 線程B讀一個volatile變量,實質上是線程B接收了以前某個線程發出的(在寫這個volatile變量以前對共享變量所作修改的)消息
  • 線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過主內存向線程B發送消息。

看到這裏,你們對JMM的共享內存模型隱式通訊的特色是否是認識更深入些了?

內存屏障

上面介紹完了volatile的內存語義,接下來看看JMM如何實現volatile的內存語義。以前咱們提到太重排序分爲編譯器重排序和處理器重排序,爲了實現volatile內存語義,JMM會限制這兩種重排序類型。


能夠看出來

  • 第二個操做是volatile寫時候,無論第一個操做是啥,都不能重排序
  • 第一個操做是volatile讀時,無論第二個操做是啥,都不能重排序
  • 第一個操做是volatile寫,第二個是volatile讀時,不能重排序

編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止處理器重排序。內存屏障的做用有兩個:1是阻止屏障兩側的的指令重排,2是強制把高速緩存中的數據更新或者寫入到主存中。Load Barrier負責更新高速緩存, Store Barrier負責將高速緩衝區的內容寫回主存

JMM會採起保守策略,保證任何狀況下都能獲得正確的volatile語義。

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障。
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障。這一步主要目的是防止volatile寫和可能有的volatile讀發生重排序
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障。
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障。

這裏以volatile寫爲例,講解下屏障的意義。以下圖所示,volatile寫的先後分別被插入了StoreStore和StoreLoad屏障,插入以後,處理器就不能對指定類型進行重排序了。你能夠把內存屏障看作一個欄杆,代碼想越也越不了,只能按照固定順序執行。


可能看到這裏你就會有疑問了,上面咱們講到第二個操做是volatile寫時候,無論第一個操做是啥,都不能重排序,可是這裏只給volatile寫前面加了個StoreStore,爲啥沒有LoadStore呢?其實這裏我也沒有想明白,歡迎理解了的同窗指點一下。

鎖內存語義

鎖是java中最重要的同步機制。你們可能從一開始學java就會把synchronized用上。接下來咱們就介紹下鎖的內存語義。

當線程釋放鎖的時候,JMM會把線程對應的本地內存中的共享變量刷新到主內存上。當另外一個線程獲取鎖的時候,JMM會把該線程對應的本地內存置爲無效,從而被監視器保護的臨界區代碼必須從主內存中讀取共享變量。這點咱們也能夠從happens-before規則中推導出來。


按照程序順序規則,1 happens-before 2 happens-before 3, 4 happens-before 5 happens-before 6。 按照鎖規則,3 happens-before 4。 因此按照傳遞規則,2 happens-before 5。也就是說,前一個線程在臨界區進行的修改,都會後續得到鎖的線程可見。

看到這裏,你們是否是以爲加鎖和釋放鎖,和上面的volatile讀和寫操做是分別對應的,具備同樣的內存語義。總結以下:

  • 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出來(其對共享變量所作修改的)消息
  • 線程B獲取一個鎖,實質上是線程B接收了以前某個線程發出的(在釋放這個鎖以前對共享變量所作修改的)消息
  • 線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A經過主內存向線程B發送消息。

鎖語義的實現

下面咱們藉助於ReentrantLock來分析鎖內存語義的具體實現機制。ReentrantLock依賴於AbstractQueuedSynchronizer(後面簡稱AQS)。AQS使用整型的volatile變量state來維護同步狀態。ReentrantLock調用方式分爲公平鎖和非公平鎖:

  • 公平鎖:在鎖的最後寫volatile變量state,在獲取鎖時首先讀這個volatile變量。根據上面咱們講到的volatile的happens-before規則,釋放鎖的線程在寫volatile變量以前對共享變量進行的修改,在別的線程讀取同一個volatile變量後將當即可見。
  • 非公平鎖:非公平鎖的釋放和公平鎖徹底同樣,而獲取則採用的是compareAndSet方法。

這裏咱們就引入了compareAndSet方法,這個方法也常被縮寫爲CAS。它的做用是,若是當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值。能夠看到CAS操做知足了原子性和可見性。處理器在處理CAS方法時候,會給交換指令加上lock前綴。而lock前綴的特徵以下:

  • 確保對內存的讀-改-寫操做原子執行
  • 禁止該指令,與以前和以後的讀和寫
  • 把寫緩衝區中的全部數據刷新到內存中

後面兩點就使得CAS徹底具備了volatile讀和寫的內存語義。

所以,鎖的內存語義的實現實際上能夠有兩種方式:

  • 利用volatile變量的寫-讀具備的內存語義
  • 利用CAS附帶的volatile寫和讀的內存語義。

固然還能夠利用volatile和CAS的自由組合。咱們在分析concurrent包的源代碼實現時候,就會發現通用化的實現模式:

  1. 聲明共享變量爲volatile
  2. 使用CAS的原子條件更新來實現線程之間的同步
  3. 配合volatile的讀/寫和CAS來實現線程之間的通訊。

能夠說,volatile和CAS就是java併發編程的基石


線程與併發

上面講完了內存模型,下面咱們來說講偏java實現的概念,即併發編程中的線程是如何工做的。這裏我主要是講到線程的監視器等待/通知機制

監視器

任何一個對象都有本身的監視器。你們能夠看看Object的幾個方法,wait(), notify(), notifyAll(), 這是都是跟監視器相關的。當這個對象由同步塊或者同步方法調用時,執行方法的線程必須先獲取到監視器才能進入同步區域。咱們能夠把監視器看作房間的大門。只有拿到門鎖才能進入房間。而沒有取到監視器的線程則會阻塞在入口處,線程會進入BLOCK狀態。咱們就會說線程被阻塞了。


等待/通知機制

你們都知道,咱們常使用的synchronized就是一個單線程監視器鎖,只有一個線程能得到監視器並進入臨界區執行代碼,而其餘的線程則會進入到同步隊列中,等待線程釋放了監視器之後,收到Monitor.Exit通知才能繼續去嘗試得到監視器。

那麼問題來了,這裏不是隻要等着線程A釋放監視器,線程B就能去獲取鎖了嗎,那麼Object的wait和notify操做是拿來幹嗎的呢?

其實現實狀態並不會像想的那麼理想,線程B獲取到了鎖,可是這時候可能條件並不知足,線程B並不能往下執行。好比線程B只能在flag=true的狀況下執行,可是當它獲取到監視器的時候,flag=false,那麼若是線程B實在是想執行後面的操做的話,就有兩種辦法:

  • 佔住監視器,循環等待,每隔一段時間判斷flag是否爲true
  • 釋放監視器,等flag爲true的時候,由別的對象通知它,它再去獲取監視器執行。

可想而知,第二種效率更高,更及時。這也是咱們說到的等待/通知機制

這裏咱們看到,當條件不知足的時候,能夠經過條件變量的wait()方法將當前正在執行的線程放入條件變量的等待隊列,當條件知足的時候,調用條件變量的notify或者notifyAll方法將線程從等待隊列中移出。那麼有些同窗就會犯迷糊了。這裏的等待隊列和上面說到的由於沒獲取到監視器的線程的阻塞隊列有區別嗎?

固然有區別!上面的阻塞隊列是沒獲取到監視器,這裏的等待隊列是獲取到監視器,可是繼續運行的條件沒有知足,所以本身陷入等待狀態的隊列。二者是不一樣的概念。以追妹子爲例,咱們能夠理解爲阻塞隊列是排隊進妹子家的大門,進了大門才能和妹子聊天,可是聊完以後就要進入到備胎池等着妹子選擇佳婿了。等妹子選好之後接到妹子電話,就能夠從新排隊登門和妹子談戀愛了。


使用wait(),notify()和notifyAll()有些須要注意的細節:

  • 使用這三個方法須要先對調用對象加鎖
  • 調用wait方法後,線程狀態由Running變爲Waiting,並將當前線程放置到對象的等待隊列
  • notify或notifyAll方法調用後,等待線程不會從wait返回,而是要從新獲取到鎖之後才能返回。
  • notify將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll是將全部線程所有移到同步隊列。

三.總結

本文講到了java內存模型和線程的同步機制。java內存模型的重點是happens-before規則,volatile和鎖。只要這些東西能瞭解,concurrent包裏的實現範式咱們就能看懂了。瞭解了線程的等待/通知模型,咱們就能對鎖的使用更加的瞭然於心。

本文就到這裏啦。後續可能會學習下java的併發工具的設計和實現,若是有值得分享的東西,會另外再寫文章分享出來,敬請期待。

參考

《java併發編程的藝術》
《java併發編程實戰》王寶令

我是Android笨鳥之旅,笨鳥也要有向上飛的心,我在這裏陪你一塊兒慢慢變強。期待你的關注

相關文章
相關標籤/搜索