JUC簡介

bluesmi發表於2018-12-11

JUC簡介

       在Java 5.0 提供了java.util.concurrent(簡稱JUC )包,在此包中增加了在併發程式設計中很常用的實用工具類,用於定義類似於執行緒的自定義子系統,包括執行緒池、非同步IO 和輕量級任務框架。提供可調的、靈活的執行緒池。還提供了設計用於多執行緒上下文中的Collection 實現等。

  • 記憶體可見性問題: 當多個執行緒操作共享資料時,彼此不可見

  • volatile 關鍵字: 當多個執行緒進行操作共享資料時,可以保證記憶體中的資料可見。 相較於 synchronized 是一種較為輕量級的同步策略。

    注意: 1. volatile 不具備“互斥性” 2. volatile 不能保證變數的“原子性”

  • i++ 的原子性問題:i++ 的操作實際上分為三個步驟“讀-改-寫”

    	      int i = 10;
    	      i = i++; //10
    	      int temp = i;
    	      i = i + 1;
    	      i = temp;
    複製程式碼
  • 原子變數:在 java.util.concurrent.atomic 包下提供了一些原子變數。

    1. volatile 保證記憶體可見性
    2. CAS(Compare-And-Swap) 演算法保證資料變數的原子性 CAS 演算法是硬體對於併發操作的支援 CAS 包含了三個運算元: ①記憶體值 V ②預估值 A ③更新值 B 當且僅當 V == A 時, V = B; 否則,不會執行任何操作。
    3. CAS演算法要比同步鎖的效率高很多,因為執行緒不會阻塞,它可以馬上去讀,然後更新值。
    4. CAS也是硬體對於併發操作的支援
  • CopyOnWriteArrayList/CopyOnWriteArraySet : “寫入並複製” 注意:新增操作多時,效率低,因為每次新增時都會進行復制,開銷非常的大。併發迭代操作多時可以選擇。

  • CurrentHashMap: - 參考1 - 參考2

  • java建立執行緒的4中方式

    1. 繼承Thread類 (1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。 (2)建立Thread子類的例項,即建立了執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。
    2. 通過Runnable介面建立執行緒類 (1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。 (2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。
    3. 通過Callable和Future建立執行緒 (1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。 (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。 (3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。 (4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值 (5)FutureTask也可以用於閉鎖的操作。
  • 執行緒池方式

    • 對比:
      • 採用實現Runnable、Callable介面的方式創見多執行緒時。
        • 優勢是:
          • 執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。
          • 在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了物件導向的思想。
        • 劣勢是:    程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。
      • 使用繼承Thread類的方式建立多執行緒時。
        • 優勢: 編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。
        • 劣勢: 執行緒類已經繼承了Thread類,所以不能再繼承其他父類。
  • 用於解決多執行緒執行緒安全的方式:

    • jdk1.5以前:
      • Synchronize:隱式鎖
        • 同步程式碼塊
        • 同步方法
    • jdk1.5以後:
      • 同步鎖lock:
        • 顯示鎖,必須通過lock()方法進行上鎖,同時也必須通過unLock()方法釋放鎖
        • 問題:需要保證鎖會釋放,所以一般unLock()要放在finally下面
  • 生產者與消費者

    • 不用等待喚醒機制,會產生的問題:
      • 重複呼叫佔用資源問題

        • 原因分析 : 上述的情況是當沒貨的時候還會繼續呼叫該方法,從而佔用資源,二貨滿的情況下也會重複呼叫進貨方法,佔用資源,這樣是不合理的。
        • 解決方式: 當貨滿了,應該停止進貨,釋放鎖讓消費者消費,當沒貨了應該停止消費釋放鎖,讓進貨,這是我們想要的邏輯。使用wait()和notifyAll()這兩個方法來實現。
      • 執行緒阻塞無法喚醒

        • 原因分析 當product比較小假如是1的時候,有可能生產者先迴圈結束, 消費者還沒結束,一直在waite無法得到喚醒就一直等待 程式就會停在那裡
        • 解決方式 去掉else,保證每次都會喚醒另外一個執行緒
      • 虛假喚醒問題 當只有一個Factory有兩個Consumer的時候就會出現虛假喚醒問題。導致商品都成了負數了。

        • 原因分析: 當建立對個生產消費者執行緒的時候,會產生虛假喚醒,導致product 為負數,是因為當消費者執行緒A發現沒貨的時候,wait之後釋放鎖, 另外一個消費者執行緒B獲得鎖開始執行,結果也沒貨,開始wait,當生產者生產之後notifyAll,A,B執行緒開始繼續向下執行,結果進行了兩次–操作,導致product成為了負數
        • 解決方式: JDK文件object的wait方法已經考慮到這種情況,防止虛假喚醒,應該放在迴圈中,多次進行檢查,直到滿足條件才進行下一步。即不要使用if來進行判斷而用while迴圈來進行判斷
      • 守護執行緒解決執行緒阻塞 上面解決了虛假喚醒問題,但是當多個消費者和一個生產者的時候,生產者有可能先結束迴圈,但是消費者還沒結束,結果到了其他消費者的時候發現product是小於0的於是就wait,程式一直等待得不到結束,就會一直在wait()

        • 解決方式: 在共享資源clerk類中定義生產者執行緒標誌位,在main執行緒中建立一個執行緒設定為守護執行緒並啟動,在該守護執行緒中建立匿名內部類Runnable並在run方法中判斷生產者執行緒isAlive()如果生產者執行緒結束,就把標誌位置為false,該標識位和消費者執行緒的while判斷條件中串聯。當生產者執行緒為false的之後短路,使得消費和執行緒啥都不做,直到執行緒結束。

          • Clerk中設定Factory執行緒的標誌位
              private boolean facctoryFlg = true;//工廠執行緒結束的標誌位,為false表示執行緒執行完畢
                  public boolean isFacctoryFlg() {
                      return facctoryFlg;
                  }
                  public void setFacctoryFlg(boolean facctoryFlg) {
                      this.facctoryFlg = facctoryFlg;
                  }     
          複製程式碼
          • 主方法中建立守護執行緒
           //建立守護執行緒
                    Thread daemon = new Thread(new Runnable() {
                        @Override
                        public void run() {
                           while(true){
                              if(!tf.isAlive()){
                                  clerk.setFacctoryFlg(false);
                                  System.out.println("factory--------------"+tf.isAlive());
                                  break;
                              }
                           }
                        }
                    });
                    daemon.setDaemon(true);//設定為守護執行緒(後臺執行緒)
                    daemon.start();
          複製程式碼
          • 修改Clerk的sale方法:
          //售貨
              public synchronized void sale(){
          
                  while(product<=0){
                      //當Factory執行緒結束的時候,直接結束sale方法
                      if(!isFacctoryFlg()){
                          return;
                      }
                      System.out.println("沒貨了");
                      try {
                          this.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                      System.out.println(Thread.currentThread().getName()+"賣貨"+product);
                      --product;
                      notifyAll();
                  }
            	  ```
          複製程式碼
      通過守護執行緒daemon的監視,可以避免執行緒阻塞的情況,就算有多個消費者或者Factory只要在守護執行緒中新增判斷邏輯,就可以避免阻塞的出現。