java併發包小結(一)

java.util.concurrent 包含許多線程安全、高性能的併發構建塊。換句話講,創建 java.util.concurrent 的目的就是要實現 Collection 框架對數據結構所執行的併發操作。通過提供一組可靠的、高性能併發構建塊,開發人員可以提高併發類的線程安全、可伸縮性、性能、可讀性和可靠性。


JDK 5.0 中的併發改進可以分爲三組:
   
1. JVM 級別更改。大多數現代處理器對併發對某一硬件級別提供支持,通常以 compare-and-swap (CAS)指令形式。CAS 是一種低級別的、細粒度的技術,它允許多個線程更新一個內存位置,同時能夠檢測其他線程的衝突並進行恢復。它是許多高性能併發算法的基礎。在 JDK 5.0 之前,Java 語言中用於協調線程之間的訪問的惟一原語是同步,同步是更重量級和粗粒度的。公開 CAS 可以開發高度可伸縮的併發 Java 類。這些更改主要由 JDK 庫類使用,而不是由開發人員使用。

   2. 低級實用程序類 -- 鎖定和原子類。使用 CAS 作爲併發原語,ReentrantLock 類提供與 synchronized 原語相同的鎖定和內存語義,然而這樣可以更好地控制鎖定(如計時的鎖定等待、鎖定輪詢和可中斷的鎖定等待)和提供更好的可伸縮性(競爭時的高性能)。大多數開發人員將不再直接使用 ReentrantLock 類,而是使用在 ReentrantLock 類上構建的高級類。

   3. 高級實用程序類。這些類實現併發構建塊,每個計算機科學文本中都會講述這些類 -- 信號、互斥、閂鎖、屏障、交換程序、線程池和線程安全集合類等。大部分開發人員都可以在應用程序中用這些類,來替換許多(如果不是全部)同步、wait() 和 notify() 的使用,從而提高性能、可讀性和正確性。


什麼是線程?
進程:獨立運行的程序,在某種程度上相互隔離。
線程有時稱爲輕量級進程。與進程一樣,它們擁有通過程序運行的獨立的併發路徑,並且每個線程都有自己的程序計數器,稱爲堆棧和本地變量。然而,線程存在於進程中,它們與同一進程內的其他線程共享內存、文件句柄以及進程狀態。
今天,幾乎所有操作系統都支持線程,允許執行多個可獨立調度的線程,以便共存於一個進程中。因爲一個進程中的所有線程是在同一個地址空間中執行的,所以多個線程可以同時訪問相同對象,並且它們從同一堆棧中分配對象。雖然這使線程更易於與其他線程共享信息,但也意味着您必須確保線程之間不相互干涉。
正確使用線程能帶來諸多好處,其中包括更好的資源利用、簡化開發、高吞吐量、更易響應的用戶界面以及能執行異步處理。


線程的優點:
1. 使用多處理器。 多處理器(MP)系統變得越來越便宜,並且分佈越來越廣泛。因爲調度的基本單位通常是線程,所以不管有多少處理器可用,一個線程的應用程序一次只能在一個處理器上運行。在設計良好的程序中,通過更好地利用可用的計算機資源,多線程能夠提高吞吐量和性能。
2. 簡化建模。 有效使用線程能夠使程序編寫變得更簡單,並易於維護
3. 異步或後臺處理。 服務器應用程序可以同時服務於許多遠程客戶機。如果應用程序從 socket 中讀取數據,並且沒有數據可以讀取,那麼對 read() 的調用將被阻塞,直到有數據可讀。在單線程應用程序中,這意味着當某一個線程被阻塞時,不僅處理相應請求要延遲,而且處理所有請求也將延遲。然而,如果每個 socket 都有自己的 IO 線程,那麼當一個線程被阻塞時,對其他併發請求行爲沒有影響。


一些特殊場景:

a)Servlet 和 JavaServer Page 技術
Servlet 容器可以創建多個線程,在多個線程中同時調用給定 servlet,從而處理多個請求。因此 servlet 類必須是線程安全的。
b)RMI
遠程方法調用(remote method invocation,RMI)工具允許調用其他 JVM 中運行的操作。實現遠程對象最普遍的方法是擴展 UnicastRemoteObject。例示 UnicastRemoteObject 時,它是通過 RMI 調度器註冊的,該調度器可能創建一個或多個線程,將在這些線程中執行遠程方法。因此,遠程類必須是線程安全的。

正如所看到的,即使應用程序沒有明確創建線程,也會發生許多可能會從其他線程調用類的情況。幸運的是,java.util.concurrent 中的類可以大大簡化編寫線程安全類的任務。


例子 -- 非線程安全 servlet
下列 servlet 看起來像無害的留言板 servlet,它保存每個來訪者的姓名。然而,該 servlet 不是線程安全的,而這個 servlet 應該是線程安全的。問題在於它使用 HashSet 存儲來訪者的姓名,HashSet 不是線程安全的類。
當我們說這個 servlet 不是線程安全的時,是說它所造成的破壞不僅僅是丟失留言板輸入。在最壞的情況下,留言板數據結構都可能被破壞並且無法恢復。

public class UnsafeGuestbookServlet extends HttpServlet {

    private Set visitorSet = new HashSet();

    protected void doGet(HttpServletRequest httpServletRequest,

             HttpServletResponse httpServletResponse) throws ServletException, IOException {

        String visitorName = httpServletRequest.getParameter('NAME');

        if (visitorName != null)

            visitorSet.add(visitorName);

    }

}

通過將 visitorSet 的定義更改爲下列代碼,可以使該類變爲線程安全的:
private Set visitorSet = Collections.synchronizedSet(new HashSet());


線程安全集合
JDK 1.2 中引入的 Collection 框架是一種表示對象集合的高度靈活的框架,它使用基本接口 List、Set 和 Map。通過 JDK 提供每個集合的多次實現(HashMap、Hashtable、TreeMap、WeakHashMap、HashSet、TreeSet、Vector、ArrayList、LinkedList 等等)。其中一些集合已經是線程安全的(Hashtable 和 Vector),通過同步的封裝工廠(Collections.synchronizedMap()、synchronizedList() 和 synchronizedSet()),其餘的集合均可表現爲線程安全的。
java.util.concurrent 包添加了多個新的線程安全集合類(ConcurrentHashMap、CopyOnWriteArrayList 和 CopyOnWriteArraySet)。這些類的目的是提供高性能、線程安全的基本集合類。
java.util 中的線程集合仍有一些缺點。例如,在迭代鎖定時,通常需要將該鎖定保留在集合中,否則,會有拋出 ConcurrentModificationException 的危險。(這個特性有時稱爲條件線程安全;有關的更多說明,請參閱參考資料。)此外,如果從多個線程頻繁地訪問集合,則常常不能很好地執行這些類。java.util.concurrent 中的新集合類允許通過在語義中的少量更改來獲得更高的併發。
 JDK 5.0 還提供了兩個新集合接口 -- Queue 和 BlockingQueue。Queue 接口與 List 類似,但它只允許從後面插入,從前面刪除。通過消除 List 的隨機訪問要求,可以創建比現有 ArrayList 和 LinkedList 實現性能更好的 Queue 實現。因爲 List 的許多應用程序實際上不需要隨機訪問,所以Queue 通常可以替代 List,來獲得更好的性能。


CopyOnWriteArrayList 和 CopyOnWriteArraySet
可以用兩種方法創建線程安全支持數據的 List -- Vector 或封裝 ArrayList 和 Collections.synchronizedList()。java.util.concurrent 包添加了名稱繁瑣的 CopyOnWriteArrayList。爲什麼我們想要新的線程安全的List類?爲什麼Vector還不夠?
最簡單的答案是與迭代和併發修改之間的交互有關。使用 Vector 或使用同步的 List 封裝器,返回的迭代器是 fail-fast 的,這意味着如果在迭代過程中任何其他線程修改 List,迭代可能失敗。
Vector的非常普遍的應用程序是存儲通過組件註冊的監聽器的列表。當發生適合的事件時,該組件將在監聽器的列表中迭代,調用每個監聽器。爲了防止ConcurrentModificationException,迭代線程必須複製列表或鎖定列表,以便進行整體迭代,而這兩種情況都需要大量的性能成本。
CopyOnWriteArrayList類通過每次添加或刪除元素時創建支持數組的新副本,避免了這個問題,但是進行中的迭代保持對創建迭代器時的當前副本進行操作。雖然複製也會有一些成本,但是在許多情況下,迭代要比修改多得多,在這些情況下,寫入時複製要比其他備用方法具有更好的性能和併發性。
如果應用程序需要 Set 語義,而不是 List,那麼還有一個 Set 版本 -- CopyOnWriteArraySet。


ConcurrentHashMap
正如已經存在線程安全的 List 的實現,您可以用多種方法創建線程安全的、基於 hash 的 Map -- Hashtable,並使用 Collections.synchronizedMap() 封裝 HashMap。JDK 5.0 添加了 ConcurrentHashMap 實現,該實現提供了相同的基本線程安全的 Map 功能,但它大大提高了併發性。
Hashtable 和 synchronizedMap 所採取的獲得同步的簡單方法(同步 Hashtable 中或者同步的 Map 封裝器對象中的每個方法)有兩個主要的不足。首先,這種方法對於可伸縮性是一種障礙,因爲一次只能有一個線程可以訪問 hash 表。同時,這樣仍不足以提供真正的線程安全性,許多公用的混合操作仍然需要額外的同步。雖然諸如 get() 和 put() 之類的簡單操作可以在不需要額外同步的情況下安全地完成,但還是有一些公用的操作序列,例如迭代或者 put-if-absent(空則放入),需要外部的同步,以避免數據爭用。
Hashtable 和 Collections.synchronizedMap, 通過同步每個方法獲得線程安全。這意味着當一個線程執行一個 Map 方法時,無論其他線程要對 Map 進行什麼樣操作,都不能執行,直到第一個線程結束纔可以。
對比來說,ConcurrentHashMap 允許多個讀取幾乎總是併發執行,讀和寫操作通常併發執行,多個同時寫入經常併發執行。結果是當多個線程需要訪問同一 Map 時,可以獲得更高的併發性。
在大多數情況下,ConcurrentHashMap 是 Hashtable或 Collections.synchronizedMap(new HashMap()) 的簡單替換。然而,其中有一個顯著不同,即 ConcurrentHashMap 實例中的同步不鎖定映射進行獨佔使用。實際上,沒有辦法鎖定 ConcurrentHashMap 進行獨佔使用,它被設計用於進行併發訪問。爲了使集合不被鎖定進行獨佔使用,還提供了公用的混合操作的其他(原子)方法,如 put-if-absent。ConcurrentHashMap 返回的迭代器是弱一致的,意味着它們將不拋出ConcurrentModificationException ,將進行'合理操作'來反映迭代過程中其他線程對 Map 的修改。


隊列
原始集合框架包含三個接口:List、Map 和 Set。List 描述了元素的有序集合,支持隨即訪問 -- 可以在任何位置添加、提取或刪除元素。
LinkedList 類經常用於存儲工作元素(等待執行的任務)的列表或隊列。然而,List 提供的靈活性比該公用應用程序所需要的多得多,這個應用程序通常在後面插入元素,從前面刪除元素。但是要支持完整 List 接口則意味着 LinkedList 對於這項任務不像原來那樣有效。Queue 接口比 List 簡單得多,僅包含 put() 和 take() 方法,並允許比 LinkedList 更有效的實現。
Queue 接口還允許實現來確定存儲元素的順序。ConcurrentLinkedQueue 類實現先進先出(first-in-first-out,FIFO)隊列,而 PriorityQueue 類實現優先級隊列(也稱爲堆),它對於構建調度器非常有用,調度器必須按優先級或預期的執行時間執行任務。

interface Queue extends Collection {

    boolean offer(E x);

    E poll();

    E remove() throws NoSuchElementException;

    E peek();

    E element() throws NoSuchElementException;

}
實現 Queue 的類是:
1. LinkedList 已經進行了改進來實現 Queue。
2. PriorityQueue 非線程安全的優先級對列(堆)實現,根據自然順序或比較器返回元素。
3. ConcurrentLinkedQueue 快速、線程安全的、無阻塞 FIFO 隊列。


線程創建
通過例示從 Thread 獲得的對象並調用 Thread.start() 方法來創建線程。可以用兩種方法創建線程:通過擴展 Thread 和覆蓋 run() 方法,或者通過實現 Runnable 接口和使用 Thread(Runnable) 構造函數:

class WorkerThread extends Thread {
  public void run() { /* do work */ }
}
Thread t = new WorkerThread();
t.start();
或者:
Thread t = new Thread(new Runnable() {
  public void run() { /* do work */ }
}
t.start();


如何不對任務進行管理
大多數服務器應用程序(如 Web 服務器、POP 服務器、數據庫服務器或文件服務器)代表遠程客戶機處理請求,這些客戶機通常使用 socket 連接到服務器。對於每個請求,通常要進行少量處理(獲得該文件的代碼塊,並將其發送回 socket),但是可能會有大量(且不受限制)的客戶機請求服務。
用於構建服務器應用程序的簡單化模型會爲每個請求創建新的線程。下列代碼段實現簡單的 Web 服務器,它接受端口 80 的 socket 連接,並創建新的線程來處理請求。不幸的是,該代碼不是實現 Web 服務器的好方法,因爲在重負載條件下它將失敗,停止整臺服務器。

class UnreliableWebServer {
  public static void main(String[] args) {
    ServerSocket socket = new ServerSocket(80);
      while (true) {
      final Socket connection = socket.accept();
      Runnable r = new Runnable() {
        public void run() {
          handleRequest(connection);
        }
      };
      // Don't do this!
      new Thread(r).start();
    }
  }
}
當服務器被請求吞沒時,UnreliableWebServer 類不能很好地處理這種情況。每次有請求時,就會創建新的類。根據操作系統和可用內存,可以創建的線程數是有限的。
不幸的是,您通常不知道限制是多少 -- 只有當應用程序因爲 OutOfMemoryError 而崩潰時才發現。
如果足夠快地在這臺服務器上拋出請求的話,最終其中一個線程創建將失敗,生成的 Error 會關閉整個應用程序。當一次僅能有效支持很少線程時,沒有必要創建上千個
線程,無論如何,這樣使用資源可能會損害性能。創建線程會使用相當一部分內存,其中包括有兩個堆棧(Java 和 C),以及每線程數據結構。如果創建過多線程,其中
每個線程都將佔用一些 CPU 時間,結果將使用許多內存來支持大量線程,每個線程都運行得很慢。這樣就無法很好地使用計算資源。

使用線程池解決問題
爲任務創建新的線程並不一定不好,但是如果創建任務的頻率高,而任務平均持續時間低,我們可以看到每項任務創建一個新的線程將產生性能(如果負載不可預知,還有穩定性)問題。
如果不是每項任務創建一個新的線程,則服務器應用程序必須採取一些方法來限制一次可以處理的請求數。這意味着每次需要啓動新的任務時,它不能僅調用下列代碼。
new Thread(runnable).start()
管理一大組小任務的標準機制是組合工作隊列和線程池。工作隊列就是要處理的任務的隊列,前面描述的 Queue 類完全適合。線程池是線程的集合,每個線程都提取公用工作隊列。當一個工作線程完成任務處理後,它會返回隊列,查看是否有其他任務需要處理。如果有,它會轉移到下一個任務,並開始處理。
線程池爲線程生命週期間接成本問題和資源崩潰問題提供瞭解決方案。通過對多個任務重新使用線程,創建線程的間接成本將分佈到多個任務中。作爲一種額外好處,因爲請求到達時,線程已經存在,從而可以消除由創建線程引起的延遲。因此,可以立即處理請求,使應用程序更易響應。而且,通過正確調整線程池中的線程數,可以強制超出特定限制的任何請求等待,直到有線程可以處理它,它們等待時所消耗的資源要少於使用額外線程所消耗的資源,這樣可以防止資源崩潰。


Executor 框架
java.util.concurrent 包中包含靈活的線程池實現,但是更重要的是,它包含用於管理實現 Runnable 的任務的執行的整個框架。該框架稱爲 Executor 框架。
Executor 接口相當簡單。它描述將運行 Runnable 的對象:

public interface Executor {
  void execute(Runnable command);
}
任務運行於哪個線程不是由該接口指定的,這取決於使用的 Executor 的實現。
java.util.concurrent 中的大多數 Executor 實現還實現 ExecutorService 接口,這是對 Executor 的擴展,它還管理執行服務的生命週期。這使它們更易於管理,並向生命可能比單獨 Executor 的生命更長的應用程序提供服務。

public interface ExecutorService extends Executor {

  void shutdown();

  List shutdownNow();

  boolean isShutdown();

  boolean isTerminated();

  boolean awaitTermination(long timeout,

                           TimeUnit unit);

  // other convenience methods for submitting tasks

}
java.util.concurrent 包含多個 Executor 實現,每個實現都實現不同的執行策略。什麼是執行策略?執行策略定義何時在哪個線程中運行任務,執行任務可能消耗的資源級別(線程、內存等等),以及如果執行程序超載該怎麼辦。
執行程序通常通過工廠方法示例,而不是通過構造函數。Executors 類包含用於構造許多不同類型的 Executor 實現的靜態工廠方法:
1. Executors.newCachedThreadPool() 創建不限制大小的線程池,但是當以前創建的線程可以使用時將重新使用那些線程。如果沒有現有線程可用,
將創建新的線程並將其添加到池中。使用不到 60 秒的線程將終止並從緩存中刪除。
2. Executors.newFixedThreadPool(int n) 創建線程池,其重新使用在不受限制的隊列之外運行的固定線程組。在關閉前,所有線程都會因爲執行
過程中的失敗而終止,如果需要執行後續任務,將會有新的線程來代替這些線程。
3. Executors.newSingleThreadExecutor() 創建單一工作線程,與 Swing 事件線程非常相似。保證順序執行任務,在任何給定時間,不會有多個任務處於活動狀態。

之前的例子可以做簡單調整,只需將 Thread.start() 調用替換爲向 Executor 提交任務即可:
class ReliableWebServer {

  Executor pool = Executors.newFixedThreadPool(7);

    public static void main(String[] args) {

    ServerSocket socket = new ServerSocket(80);

      while (true) {

      final Socket connection = socket.accept();

      Runnable r = new Runnable() {

        public void run() {

          handleRequest(connection);

        }

      };

      pool.execute(r);

    }

  }

}

續篇:java併發包小結(二) 

http://blog.csdn.net/aalansehaiyang52/article/details/8971992


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