Java併發編程實戰筆記1.0

1.併發編程領域可以抽象成三個核心問題:分工、同步和互斥

分工指的是如何高效地拆解任務並分配給線程,而同步指的是線程之間如何協作,互斥則是保證同一時刻只允許一個線程訪問共享資源。

2.併發編程領域問題的產生原因:

源頭之一:緩存導致的可見性問題

    一個線程對共享變量的修改,另外一個線程能夠立刻看到,我們稱爲可見性。

源頭之二:線程切換帶來的原子性問題

    我們把一個或者多個操作在CPU執行的過程中不被中斷的特性稱爲原子性。

源頭之三:編譯優化帶來的有序性問題

    編譯器爲了優化性能,有時候會改變程序中語句的先後順序,編譯器調整了語句的順序,大多數情況下不影響程序的最終結果。不過有時候編譯器及解釋器的優化可能導致意想不到的 Bug。

緩存導致的可見性問題,線程切換帶來的原子性問題,編譯優化帶來的有序性問題,其實緩存、線程、編譯優化的目的和我們寫併發程序的目的是相同的,都是提高程序性能。但是技術在解決一個問題的同時,必然會帶來另外一個問題,所以在採用一項技術的同時,一定要
清楚它帶來的問題是什麼,以及如何規避。

解決可見性、有序性合理的方案應該是按需禁用緩存以及編譯優化。解決原子性問題需要互斥鎖。

3.死鎖

死鎖的定義是:一組互相競爭資源的線程因互相等待,導致“永久”阻塞的現象。
只有以下這四個條件都發生時纔會出現死鎖:
    互斥,共享資源 X 和 Y 只能被一個線程佔用;
    佔有且等待,線程 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
    不可搶佔,其他線程不能強行搶佔線程 T1 佔有的資源;
    循環等待,線程 T1 等待線程 T2 佔有的資源,線程 T2 等待線程 T1 佔有的資源,就是循環等待。

反過來分析,也就是說只要我們破壞其中一個,就可以成功避免死鎖的發生。其中,互斥這個條件我們沒有辦法破壞,因爲我們用鎖爲的就是互斥。不過其他三個條件都是有辦法破壞掉的,到底如何做呢?對於“佔用且等待”這個條件,我們可以一次性申請所有的資源,這樣
就不存在等待了。對於“不可搶佔”這個條件,佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。對於“循環等待”這個條件,可以靠按序申請資源來預防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化後自然就不存在循環了。

4.等待-通知機制

synchronized用法:


class Allocator {
  private List<Object> als;
  // 一次性申請所有資源
  synchronized void apply(bject from, Object to){
    // 經典寫法
    while(als.contains(from) ||als.contains(to)){
      try{
        wait();
      }catch(Exception e){
      }   
    } 
    als.add(from);
    als.add(to);  
  }
  // 歸還資源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

關於notify() 和notifyAll():
    notify() 是會隨機地通知等待隊列中的一個線程,而 notifyAll() 會通知等待隊列中的所有線程。從感覺上來講,應該是notify()更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但那所謂的感覺往往都蘊藏着風險,實際上使用notify()也很有風險,它的風險在於可能導致某些線程永遠不會被通知到。
    除非經過深思熟慮,否則儘量使用notifyAll()。那什麼時候可以使用 notify() 呢?需要滿足以下三個條件:
    1.所有等待線程擁有相同的等待條件;
    2.所有等待線程被喚醒後,執行相同的操作;
    3.只需要喚醒一個線程。

lock用法:


public class BlockedQueue<T>{
  final Lock lock = new ReentrantLock();
  // 條件變量:隊列不滿  
  final Condition notFull = lock.newCondition();
  // 條件變量:隊列不空  
  final Condition notEmpty = lock.newCondition();

  // 入隊
  void enq(T x) {
    lock.lock();
    try {
      while (隊列已滿){
        // 等待隊列不滿 
        notFull.await();
      }  
      // 省略入隊操作...
      //入隊後,通知可出隊
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出隊
  void deq(){
    lock.lock();
    try {
      while (隊列已空){
        // 等待隊列不空
        notEmpty.await();
      }
      // 省略出隊操作...
      //出隊後,通知可入隊
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

5.創建多少線程合適?

對於 CPU 密集型的計算場景,在工程上,線程的數量一般會設置爲“CPU核數+1”,這樣的話,當線程因爲偶爾的內存頁失效或其他原因導致阻塞時,這個額外的線程可以頂上,從而保證 CPU 的利用率。
對於 I/O 密集型的計算場景,最佳的線程數是與程序中CPU計算和I/O操作的耗時比相關的,我們可以總結出這樣一個公式:
    最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]

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