Java併發原理學習筆記+總結+實戰(2)——線程帶來的風險

1、線程帶來的風險

    java對線程的支持其實是一把雙刃劍。雖然java提供了相應的語言和庫,以及一種明確的跨平臺內存模型,這些工具簡化了併發應用程序的開發,但同時也提高的對開發人員的技術要求,因爲更多的程序中會使用線程。

1.1 線程的安全性問題

    線程安全性可能是非常複雜的,在沒有充分同步的情況下,多個線程中的操作次序是不可預測的,甚至會產生奇怪的結果。

/**
 * UnsafeSequence:線程不安全例子.
 *
 * @author YUSIR
 * @version 2019-03-01
 */
public class UnsafeSequence {
  private int value;

  /**
   * @return 返回一個唯一的數值.
   */
  public int getNext() {
    return value++;
  }
}

    上面的demo中將會產生一個整數值序列,該序列中的每個值都是唯一的。該類在單線程環境中能正常的工作,但是在多線程環境中則不能。

    UnSafeSequence的問題在於,如果執行時機不對,那麼兩個線程的調用getNext()時會得到相同的值。雖然遞增運算value++看上去是單個操作,安踏事實上包含了三個獨立操作:讀取value,將value+1,將計算結果寫入value。由於運行時可能將多個線程之間的操作交替執行,因此這兩個線程可能同時進行讀操作,從而使它們得到相同的值,並都將其加一,結果就是不同的線程返回了相同的值。

     圖中給出了不同線程之間的一種交替執行情況。執行時序按照從左到右的順序遞增,每行表示一個線程的動作。這些交替執行是給出的最糟糕的執行情況(事實上,由於指令重排序的可能,因此實際情況可能會更糟糕),目的是爲了說明,如果錯誤地假設程序中的操作將按照某種特定順序來執行,那麼會存在各種可能的、不可預知的危險。

以上說明,導致線程安全性問題出現的原因主要有:

  • 多線程環境下
  • 多個線程共享一個資源
  • 對資源進行非原子性操作

解決方案:使用synchronize進行加鎖設置,問題是,當線程加鎖後,會導致其他線程不可入,就像回到了串行的場景了,多線程就沒有意義了。

public class Sequence {

  private int value;

  /**
   * synchronized 放在普通方法上,內置鎖就是當前類的實例
   *
   * @return
   */
  public synchronized int getNext() {
    return value++;
  }

  /**
   * 修飾靜態方法,內置鎖是當前的Class字節碼對象
   * Sequence.class
   *
   * @return
   */
  public static synchronized int getPrevious() {
    //		return value --;
    return 0;
  }

  public int xx() {

    // monitorenter
    synchronized (Sequence.class) {

      if (value > 0) {
        return value;
      } else {
        return -1;
      }

    }
    // monitorexit

  }

  public static void main(String[] args) {

    Sequence s = new Sequence();
    //		while(true) {
    //			System.out.println(s.getNext());
    //		}

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

  }

}

synchronize的原理與使用

    同步代碼塊是由monitorenter和monitorexit指令實現的,當一個對象的monitor被持有後,該對象就處於鎖定狀態。

    synchronize用的鎖是存在java對象頭裏的。

                     

    在 jdk6之前的版本,synchronize是作爲一個重量級的鎖存在的(是因爲在互斥狀態下,一個線程進去了,另一個線程必須在外部等待),在之後的版本引入了幾個新的性能較高的概念

  • 偏向鎖

        每次獲取鎖和釋放鎖時會浪費資源。在很多情況下,競爭所不是由多個線程觸發的,而是由一個線程在使用。引入偏向鎖是爲了在無多線程的情況下減少不必要的輕量級鎖執行性路徑。

  • 輕量級鎖
  • 重量級鎖

1.2 活躍性問題

    安全性的含義是“永遠不發生糟糕的事情”,二活躍性則關注與另一個目標,即“某件正確的事情最終會發生”。當某個操作無法繼續執行下去時,就會發生活躍性問題。在串行程序中,活躍性問題的形式之一就是無意中造成的無限循環,從而使循環之後的代碼無法得到執行。線程將帶來其他一些活躍性問題。例如,如果線程A在等待線程B釋放其所持有的資源時,二線程B永遠都不釋放該資源,那麼A就會永久地等待下去。主要是包括:

  • 死鎖
  • 飢餓

    在Java中,下面三個常見的原因會導致線程飢餓:

        1)、高優先級吞噬所有低優先級的CPU時間片

             優先級越高的線程獲得的CPU資源越多,線程優先級設置範圍在1到10之間。

        2)、線程被永久堵塞在一個等待進入同步塊的狀態

             java同步代碼區對哪個線程執行的次序是沒有任何安排的,意味着理論上存在一個試圖進入該同步區的線程會被永久堵          塞的風險因爲其他線程總是能持續的地先於它訪問。

        3)、等待的線程永遠不會被喚醒

    如何避免飢餓問題:

        1)、設置合理的優先級

        2)、使用鎖來代替synchronize

  • 活鎖

1.3 性能問題

    性能問題包括多個方面,例如服務時間過長,響應不靈敏,吞吐率過低,資源消耗過高,或者可伸縮性較低等。使用多線程技術,在能夠提升程序性能的同時,不可避免的也會帶來某種程度上的運行時開銷。在多線程程序中,當線程調度器臨時掛起活躍線程並轉而運行另一個線程時,就會頻繁地出現上下文切換操作,這種操作將帶來極大的開銷:保存和恢復執行上下文,丟失局部性,並且CPU時間將跟過地花在線程調度而不是線程運行上。當線程共享數據時,必須使用同步機制,而這些機制往往會抑制某些編譯器的優化,是內存緩存區中的數據無效,以及增加共享內存總線的同步流量。

 

 

 

--

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章