第五章,基礎構建模組

1,同步容器類。
Vector、HashTable此類的容器是同步容器。但也有一些問題,例如,一個執行緒在使用Vector的size()方法進行迴圈每一個元素的時候,而另一個執行緒對Vector的元素進行刪除時,可能會發生ArrayIndexOutOfBoundsException。
如果要避免這個問題,可以在呼叫Vector進行迴圈的地方,對Vector例項加鎖,但效率非常差。

synchronized (vector) {
    for(int i=0; i < vector.size(); i++)
        doSomethng(vector.get(i));
}

2,迭代器與ConcurrentModificationException
Vector中有的同步問題,在許多現代的容器類中也有有類似的問題。當發現容器在迭代過程中被修改時,就會丟擲一個ConcurrentModificationException。無論直接迭代還是使用for-each迴圈,對容器類進行迭代的標準方式都是使用Iterator,這種機制的實現方式是將計數器的變化和容器關聯起來,如果迭代期間計數器被修改,那麼hasNext或next方法就會丟擲異常。
  如果不想丟擲這種異常,就要在所有使用容器的地方對容器加鎖,或是在迭代一個克隆的容器。但迭代克隆容器的方法需要考慮效能問題。

3,隱藏的迭代器

public void printSet() {
    System.out.pringln(mySet);
}

上面的程式碼中列印mySet的內容,其中System.out.pringln(mySet)這條語句是對mySet的一個隱藏的迭代,如果在這個迭代過程,有其它方法對mySet進行刪除的話,就會丟擲ConcurrentModificationException。如果使用的是同步的Set的話,就會避免這個問題。隱藏的迭代的方法還有hashCode, equals, containAll, removeAll, retainAll。

4,併發容器

  • ConcurrentHashMap:
    • 提供的迭代器不會丟擲ConcurrentModificationException,因此不需要在迭代過程對容器加鎖。它有“弱一致性”,弱一致性的迭代器可以容忍併發的修改,當建立迭代器時會遍歷已有的元素,並可以(但不保證)在迭代器被構造後將修改操作反映給容器。
    • isSize和isEmpty的語義被減弱了,以反映容器的併發特性。事實上,isSize和isEmpty在併發環境下用處很小。
    • 沒有實現對Map加鎖,以及提供獨佔訪問。在HashTable和synchronizedMap中,獲得Map的鎖能防止其它執行緒訪問這個Map。在一些不常見的情況中需要使用這種功能,例如:通過原子方式新增一些對映,或者對Map迭代並在此期間保持元素順序相同。只有當應用程式需要加鎖Map獨佔訪問時,才應該放棄使用ConcurrentHashMap。
    • 如果需要“若沒有則新增”、“若相等則移除”、“若相等則替換”等原子操作時,就要考慮使用ConcurrentHashMap了。
  • CopyOnWriteArrayList:
    • 這個容器的執行緒安全性在於,只要正確地釋出一個事實不可變的物件,那麼在訪問該物件時就不再需要進一步同步。
    • 這個容器不會丟擲ConcurrentModificationException,並且返回的元素與迭代器建立時元素完全一致,而不必考慮之後修改帶的影響。
    • BlockingQueue:適合生產者和消費者模式
  • Deque和BlockingDeque:
    • 他們分別對Queue和BlockingQueue進行擴充套件。Deque是一個雙端佇列,實現了在佇列頭和佇列尾的高效插入和移除。
    • 雙端佇列適用於另一種相關模式:工作密取(Work Stealing)。在工作密取設計中,每個消費者都有各自的雙端佇列。如果一個消費者完成了自己雙端佇列中的全部工作,那以它可以從其它消費者雙端佇列末尾祕密地獲取工作。密取工作模式比傳統的生產者-消費者模式具有更高的可伸縮性,這是因為工作者執行緒不會在單個共享的任務佇列上發生競爭。大多數時候,它們只是訪問自己的雙端佇列,從而極大地減少了競爭。當工作者執行緒需要訪問另一個佇列時,它會從佇列的尾部而不是從頭部獲取工作,因此進一步降低了佇列上的競爭程式。

5,同步工具類
同步工具類可以是任何一個物件,只要它根據其自身的狀態來協調執行緒的控制流。阻塞佇列可以作為同步工具類,其它型別的同步工具類還包括訊號量,柵欄以及閉鎖。

6,閉鎖。
閉鎖是一個同步工具類,可以延遲執行緒的進度直到其到達終止狀態。簡單地說,閉鎖可以用來確保某些活動直到其它活動都完成後才繼續執行。例如:

  • 確保某個計算在其需要的所有資源都被初始化之後才繼續執行。
  • 確保某個服務在其依賴的所有其他服務都已經啟動之後才啟動。
  • 等待直到某個操作的所有參與者都就緒再繼續執行。

CountDownLatch是一種靈活的閉鎖實現。可以在以上的情況中使用,它可以使一個或多個執行緒等待一組事件發生。

7,FutureTask
FutureTask也可用作閉鎖。FutureTask.get的行為取決於任務的狀態。如果已經完成,那麼get會立即返回結果,否則將阻塞直到任務進入完成狀態。

8,訊號量
  計數訊號量用來控制同時訪問某個特定資源的運算元量,或者同時執行某個指定操作的數量,當達到指定數量呀資源後,就阻塞住,直到有資源被釋放掉並可用後,才能繼續執行。計數訊號量還可以用來實現某種資源池,或者對容器加邊界。
  Semaphore是訊號量的一個實現。你可以使用Semaphore將任何一種容器變成有界阻塞容器。
  
注意:
只有一個資源的Semaphore和Lock非常像,但有一點區別。鎖有“可重入鎖”,但Semaphore沒有這個概念。例子如下:

  • 當你一個類的所有方法都是Synchronized的話,在一個Synchronized方法裡面,可以進入到另一個Synchronized方法裡面。他們都使用的是同一把鎖,可以進入使用這把鎖的地方。
  • 如果是使用“只有一個資源”的Semaphore來實現的話,每一個方法開始都要取得一個資源的話,在一個方法進入到另一個方法後,就執行不下去了。因為兩個方法的呼叫,使用了兩個資源。

9,柵欄
柵欄類似於閉鎖,它能阻塞一組執行緒直到某個事件發生。柵欄與閉鎖的關鍵區別在於,所有的執行緒必須同時到達柵欄才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其它執行緒。

10,構建高效且可伸縮的結果快取(例子)
當快取裡已經存在結果,就從快取裡取。如果快取裡的結果還沒有計算完成,就等待計算完成(計算可能花費時間比較長,所以做成非同步)。
這個例子有幾個問題:

  • 沒有控制啟動執行緒的數量。如果想要控制的話,可以使用訊號量控制,或者使用執行緒池體系(Executors)
  • 當快取的是 Future 而不是值時,將導致快取汙染的問題:如果某個計算取消或者失敗,那麼在計算這個結果時將指明計算過程被取消或者失敗。為了避免這種情況,如果Memoizer發現計算被取消,那麼將把Future從快取中移除。如果檢測到RuntimeException,那麼也會移除Future,這樣將來的計算才可能成功。
  • Memorizer同樣沒有解決快取逾期的問題,但它可以通過使用FutureTask的子類來解決,在子類中為每個結果指定一個逾期時間,並定期掃描快取中逾期的元素。(同樣,它也沒有解決快取清理的問題,即移除舊的計算結果以便為新的計算結果騰出空間,從而使快取不會消耗過多的記憶體)
public class Memoizer <A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache
            = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw LaunderThrowable.launderThrowable(e.getCause());
            }
        }
    }
}

11,小結
到目前為止,我們已經介紹了許多基礎知識。下面這個“併發技巧清單”列舉了在第一部分中介紹的主要概念和規則。

  • 可變狀態是至關重要的(It’s the mutable state,stupid)。
    所有的併發問題都可以歸結為如何協調對併發狀態的訪問。可變狀態越少,就越容易確保執行緒安全性。
  • 儘量將域宣告為final型別,除非需要它們是可變的。
  • 不可變物件一定是執行緒安全的。
  • 不可變物件能極大地降低併發程式設計的複雜性。它們更為簡單而且安全,可以任意共享而無須使用加鎖或保護性複製等機制。
  • 封裝有助於管理複雜性。
  • 在編寫執行緒安全的程式時,雖然可以將所有的資料都儲存在全域性變數中,但為什麼要這樣做?將資料封裝在物件中,更易於維持不變性條件:將同步機制封裝在物件中,更易於遵循同步策略。
  • 用鎖來保護每個可變變數。
  • 當保護同一個不變性條件中的所有變數時,要使用同一個鎖。
  • 在執行復合操作期間,要持有鎖。
  • 如果從多個執行緒中訪問同一個可變變數時沒有同步機制,那麼程式會出現問題。
  • 不要故作聰明地推斷出不需要使用同步。
  • 在設計過程中考慮執行緒安全,或者在文件中明確地指出它不是執行緒安全的。
  • 將同步策略文件化。

12,什麼是弱一致性

  • 集合操作是併發操作
  • 不會丟擲ConcurrentModificationException異常
  • they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.(感覺翻譯不好)

13,什麼是併發(Concurrent),什麼是同步(Synchronized)

  • 併發:是執行緒安全的,但不是通過一個獨佔鎖(single exclusion lock)進行管理控制的.
  • 同步:通過單獨鎖(single lock)來管理控制各種訪問(讀寫),可擴充套件性(scalability)低。

14,happens-before
todo