《Java併發編程實戰》學習筆記(3)

第五章:構建塊

平臺類庫包含了一個併發構建塊的豐富集合,比如線程安全容器和多種同步工具(Synchronizer)。

Synchronizer用來調節相互協作的線程間的控制流。

同步容器

同步容器類包括兩部分,一個 是VectorHashTable,它們是早期JDK的一部分;另一個是它們的同系容器,在JDK1.2中才被加入的同步包裝(wrapper)類。

這些類是由Collections.synchronizedXxx工廠方法創建的。

這些類通過封裝它們的狀態,並對每一個公共方法進行同步而實現了線程安全,這樣一次只有一個線程能訪問容器的狀態。

同步容器中出現的問題

同步容器都是線程安全的。但是對於複合操作,有時你可能需要使用額外的客戶端加鎖(client-side locking)進行保護。

通常對容器的複合操作包括:迭代(反覆獲取元素,直到獲得容器中的最後一個元素)、導航(根據一定的順序尋找下一個元素)以及條件運算,比如“缺少即加入”(put-if-absent),檢查Map中是否存在關鍵字K,如果沒有,就加入mapping (K,V)。

在一個同步的容器中,這些複合操作即使沒有客戶端加鎖的保護,技術上也是線程安全的,但是當其他線程能夠併發修改容器的時候,它們就可能不會按照你期望的方式工作了。

/**
 * 操作Vector的複合操作可能導致混亂的結果
 */
public static Object getLast(Vector list) {
  int lastIndex = list.size() - 1;
  return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
  int lastIndex = list.size() - 1;
  list.remove(lastIndex);
}

在這裏插入圖片描述

/**
 * 使用客戶端加鎖,對Vector進行復合操作
 */
public static Object getLast(Vector list) {
  synchronized (list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex) ;
  }
}
public static void deleteLast(Vector list) {
  synchronized (list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
  }
}

併發容器

Java5.0通過提供幾種併發的容器類來改進同步容器。併發容器是爲多線程併發訪問而設計的。

  • Java 5.0添加了ConcurrentHashMap,來替代同步的哈希Map實現
  • 當多數操作爲讀取操作時,CopyOnWriteArrayList來替代List相應的同步實現

新的ConcurrentMap接口加入了對常見覆合操作的支持,比如“缺少即加入(put-if-absent) ”、替換和條件刪除。

用併發容器替換同步容器,這種作法以有很小風險帶來了可擴展性顯著的提高。

Java 5.0同樣添加了兩個新的容器類型:QueueBlockingQueue

  • Queue用來臨時保存正在等待被進一步處理的一系列元素。基於Queue,有一系列的具體實現。
  • BlockingQueue擴展了Queue,增加了可阻塞的插入和獲取操作。阻塞隊列在生產者消費者設計中非常有用。

正像ConcurrentHashMap作爲同步的哈希Map的一個併發替代品,Java 6加入了ConcurrentSkipListMapConcurrentSkipListset,用來作爲同步的SortedMapSortedSet的併發替代品(比如用synchronizedMap包裝的TreeMapTreeSet)。

ConcurrentHashMap

ConcurrentHashMapHashMap一樣是一個哈希表,但是它使用完全不同的鎖策略,可以提供更好的併發性和可伸縮性。

ConcurrentHashMap使用一個更加細化的鎖機制,名叫分離鎖。這個機制允許更深層次的共享訪問。任意數量的讀線程可以併發訪問Map,讀者和寫者也可以併發訪問Map,並且有限數量的寫線程還可以併發修改Map。結果是,爲併發訪問帶來更高的吞吐量,同時幾乎沒有損失單個線程訪問的性能。

阻塞隊列和生產者-消費者模式

阻塞隊列(BlockingQueue)提供了可阻塞的puttake方法,它們與可定時的offerpoll是等價的。

阻塞隊列支持生產者-消費者設計模式。一個生產者-消費者設計分離了“識別需要完成的工作”和“執行工作”。該模式不會發現一個工作便立即處理,而是把工作置入一個任務(“to do”)清單中,以備後期處理。

生產者和消費者以不同的或者變化的速度生產和消費着數據,生產者-消費者模式將這些活動解耦,因而簡化了工作負荷的管理。

雙端隊列和工作竊取

Java 6同樣新增了兩個容器類型,Deque(發音是deck)和BlockingDeque,它們分別擴展了QueueBlockingQueue

Deque是一個雙端隊列,允許高效地在頭和尾分別進行插入和移除。實現它們的是ArrayDequeLinkedBlockingDeque

正如阻塞隊列適用於生產者-消費者模式一樣,雙端隊列使它們自身與一種叫做工作竊取(work stealing) 的模式相關聯。

一個消費者生產者設計中,所有的消費者只共享一個工作隊列;在竊取工作的設計中,每一個消費者都有一個自己的雙端隊列。如果一個消費者完成了自己雙端隊列中的全部工作,它可以偷取其他消費者的雙端隊列中的末尾任務。

因爲工作者線程並不會競爭一個共享的任務隊列,所以竊取工作模試比傳統的生產者-消費者設計有更佳的可伸縮性;大多數時候它們訪問自己的雙端隊列,減少競爭。

當一個工作者必須要訪問另一個隊列時, 它會從尾部截取,而不是從頭部,從而進一步降低對雙端隊列的爭奪。

Synchronizer

Synchronizer是一個對象,它根據本身的狀態調節線程的控制流。

阻塞隊列可以扮演一個Synchronizer的角色;其他類型的Synchronizer包括信號量( semaphore)、關卡( barrier)以及閉鎖(latch)。

在平臺類庫中存在一些Synchronizer類;如果這些不能滿足你的需要,你同樣可以按照第14章裏描述的那樣,創建一個你自己的Synchronizer

所有Synchronizer都享有類似的結構特性:它們封裝狀態,而這些狀態決定着線程執行到在某一點時是通過還是被迫等待;它們還提供操控狀態的方法,以及高效地等待Synchronizer進入到期望狀態的方法。

閉鎖

閉鎖(latch)是一種Synchronizer,它可以延遲線程的進度直到線程到達終止(terminal)狀態。

一個閉鎖工作起來就像一道大門:直到閉鎖達到終點狀態之前,門一直是關閉的,沒有線程能夠通過,在終點狀態到來的時候,門開了,允許所有線程都通過。

閉鎖可以用來確保特定活動直到其他的活動完成後才發生,比如:

  • 確保一個計算不會執行,直到它需要的資源被初始化。一個二元閉鎖(兩個狀態)可以用來表達“資源R已經被初始化”,並且所有需要用到R的活動首先都要在閉鎖中等待。
  • 確保一個服務不會開始,直到它依賴的其他服務都已經開始。每一個服務會包含一個相關的二元閉鎖;開啓服務S會首先開始等待閉鎖s中所依賴的其他服務, 在啓動結束後,會釋放閉鎖S,這樣所有依賴S的服務也可以開始處理了。
  • 等待,直到活動的所有部分都爲繼續處理作好充分準備,比如在多玩家的遊戲中的所有玩家是否都準備就緒。這樣的閉鎖會在所有玩家準備就緒時,達到終點狀態。

CountDownlatch是一個靈活的閉鎖實現,用於上述各種情況;允許一個或多個線程等待一個事件集的發生。

閉鎖的狀態包括一個計數器, 初始化爲一個正數,用來表現需要等待的事件數。

countDown方法對計數器做減操作,表示一個事件已經發生了,而await方法等待計數器達到零,此時所有需要等待的事件都已發生。如果計數器入口時值爲非零,await方法會一直阻塞直到計數器爲零,或者等待線程中斷以及超時。

FutureTask

FutureTask同樣可以作爲閉鎖。(FutureTask的實現描述了一個抽象的可攜帶結果的計算)。

FutureTask的計算是通過Callable實現的,它等價於一個可攜帶結果的Runnable,並且有3個狀態:等待、運行和完成。

完成包括所有計算以任意的方式結束,包括正常結束、取消和異常。一旦FutureTask進入完成狀態,它會永遠停止在這個狀態上。

Future.get的行爲依賴於任務的狀態。如果它已經完成,get可以立刻得到返回結果,否則會被阻塞直到任務轉入完成狀態,然後會返回結果或者拋出異常。

FutureTask把計算的結果從運行計算的線程傳送到需要這個結果的線程:FutureTask的規約保證了這種傳遞建立在結果的安全發佈基礎之上。

/**
 * 使用FutureTask預載稍後需要的數據
 */
public class Preloader {
  
  private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
    public ProductInfo call() throws DataLoadException {
      return loadProductInfo();
    }
  });
  
  private final Thread thread = new Thread(future);

  public void start() {
    thread.start();
  }

  public ProductInfo get() throws DataLoadException, InterruptedException {
    try {
      return future.get();
    } catch (ExecutionException e) {
      Throwable cause = e.getCause();
      if (cause instanceof DataLoadException)
        throw (DataLoadException) cause;
      else
        throw launderThrowable(cause);
    }
  }
}

關卡

關卡(barrier)類似於閉鎖,它們都能夠阻塞一組線程, 直到某些事件發生。

其中關卡與閉鎖關鍵的不同在於,所有線程必須同時到達關卡點,才能繼續處理。

閉鎖等待的是事件;關卡等待的是其他線程。

關卡實現的協議,就像一些家庭成員指定商場中的集合地點:”我們每個人6:00在麥當勞見,到了以後不見不散,之後我們再決定接下來做什麼。“

CyclicBarrier允許一個給定數量的成員多次集中在一個關卡點,這在並行迭代算法中非常有用,這個算法會把一個問題拆分成一系列相互獨立的子問題。

當線程到達關卡點時,調用await,await會被阻塞,直到所有線程都到達關卡點。如果所有線程都到達了關卡點,關卡就被成功地突破,這樣所有線程都被釋放,關卡會重置以備下一次使用。

如果對await的調用超時,或者阻塞中的線程被中斷,那麼關卡就被認爲是失敗的,所有對await未完成的調用都通過BrokenBarrierException終止。

如果成功地通過關卡,await爲每一個線程返回一個唯一的到達索引號, 可以用它來“選舉”產生一個領導, 在下一次迭代中承擔一些特殊工作。

/**
 * 在一個細胞的 自動系統中用CyclicBarrier協調計箅
 */
public class CellularAutomata {
  private final Board mainBoard;
  private final CyclicBarrier barrier;
  private final Worker[] workers;

  public CellularAutomata(Board board) {
    this.mainBoard = board;
    int count = Runtime.getRuntime().availableProcessors();
    this.barrier = new CyclicBarrier(count, () -> mainBoard.commitNewValues());
    this.workers = new Worker[count];
    
    for (int i = 0; i < count; i++) {
      workers[i] = new Worker(mainBoard.getSubBoard(count, i));
    }
  }

  private class Worker implements Runnable {
    private final Board board;

    public Worker(Board board) {
      this.board = board;
    }

    @Override
    public void run() {
      while (!board.hasConverged()) {
        for (int x = 0; x < board.getMaxX(); x++) {
          for (int y = 0; y < board.getMaxY(); y++) {
            board.setNewValue(x, y, computeValue(x, y));
          }
        }
        try {
          barrier.await();
        } catch (InterruptedException ex) {
          return;
        } catch (BrokenBarrierException ex) {
          return;
        }
      }
    }
    public void start() {
      for (int i = 0; i < workers.length; i++) {
        new Thread(workers[i]).start();
      }
      mainBoard.waitForConvergence();
    }
  }
}

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