java核心技術卷I-線程安全(二)

併發集視圖

假設你想要的是一個大的線程安全的集而不是映射。並沒有一個 ConcurrentHashSet 類,而且你肯定不想自己創建這樣一個類。當然,可以使用 ConcurrentHashMap (包含“ 假” 值),不過這會得到一個映射而不是集, 而且不能應用 Set 接口的操作。
靜態 newKeySet 方法會生成一個 Set, 這實際上是 ConcurrentHashMap<K, Boolean>的一個包裝器。所有映射值都爲 Boolean.TRUE, 不過因爲只是要把它用作一個集,所以並不關心具體的值

Set<String> words = ConcurrentHashMap.<String>newKeySet();

當然, 如果原來有一個映射,keySet 方法可以生成這個映射的鍵集。這個集是可變的。
如果刪除這個集的元素,這個鍵(以及相應的值)會從映射中刪除。不過,不能向鍵集增加元素,因爲沒有相應的值可以增加。Java SE 8 爲 ConcurrentHashMap 增加了第二個 keySet 方法,包含一個默認值,可以在爲集增加元素時使用:

Set<String> words = map.keySet(1L);
words.add("java");

如果 "Java”在 words 中不存在,現在它會有一個值 1。

寫數組的拷貝

CopyOnWriteArrayList 和 CopyOnWriteArraySet 是線程安全的集合,其中所有的修改線程對底層數組進行復制。如果在集合上進行迭代的線程數超過修改線程數, 這樣的安排是很有用的。當構建一個迭代器的時候, 它包含一個對當前數組的引用。如果數組後來被修改了,迭代器仍然引用舊數組, 但是,集合的數組已經被替換了。因而,舊的迭代器擁有一致的(可能過時的)視圖,訪問它無須任何同步開銷。

並行數組算法

在 Java SE 8中, Arrays 類提供了大量並行化操作。靜態 Arrays.parallelSort 方法可以對一個基本類型值或對象的數組排序。例如,

String contents = new String(Fi1es.readAl1Bytes(
Paths.get("alice.txt")), StandardCharsets.UTF_8); // Read file into string
String[] words = contents •split("[\\P{L}]+"); // Split along nonletters
Arrays,parallelSort(words);

對對象排序時,可以提供一個 Comparator。

Arrays,parallelSort(words, Comparator.comparing(String::length));

對於所有方法都可以提供一個範圍的邊界

values,parallelSort(values,length / 2, values,length); // Sort the upper half

parallelSetAll 方法會用由一個函數計算得到的值填充一個數組。這個函數接收元素索引,然後計算相應位置上的值。

Arrays.parallelSetAll(values,i-> i % 10);
// Fills values with 0 12 3 4 5 6 7 8 9 0 12 . . .

較早的線程安全集合

從 Java 的初始版本開始,Vector 和 Hashtable 類就提供了線程安全的動態數組和散列表的實現。現在這些類被棄用了, 取而代之的是 AnayList 和 HashMap 類。這些類不是線程安全的,而集合庫中提供了不同的機制。任何集合類都可以通過使用同步包裝器(synchronization wrapper) 變成線程安全的:

List<E> synchArrayList = Collections,synchronizedList(new ArrayList<E>());
Map<K , V> synchHashMap = Collections.synchronizedMap(new HashMap<K , V>());

結果集合的方法使用鎖加以保護,提供了線程安全訪問。
如果在另一個線程可能進行修改時要對集合進行迭代,仍然需要使用“ 客戶端” 鎖定:

synchronized (synchHashMap)
{
	Iterator<K> iter = synchHashMap.keySet().iterator();
	while (iter.hasNext()) . .
}

如果使用“ foreach” 循環必須使用同樣的代碼, 因爲循環使用了迭代器。注意:如果在迭代過程中,別的線程修改集合,迭代器會失效,拋出 ConcurrentModificationException 異常。同步仍然是需要的, 因此併發的修改可以被可靠地檢測出來。
最好使用 java.Util.Concurrent 包中定義的集合, 不使用同步包裝器中的。特別是, 假如它們訪問的是不同的桶, 由於 ConcurrentHashMap 已經精心地實現了,多線程可以訪問它而且不會彼此阻塞。有一個例外是經常被修改的數組列表。在那種情況下,同步的 ArrayList 可以勝過 CopyOnWriteArrayList

Callable 與 Future

Runnable 封裝一個異步運行的任務,可以把它想象成爲一個沒有參數和返回值的異步方法。Callable 與 Runnable 類似,但是有返回值。Callable 接口是一個參數化的類型,只有一個方法 call。

public interface Ca11able<V>
{
	V call() throws Exception;
}

類型參數是返回值的類型。例如, Callable 表示一個最終返回 Integer 對象的異步計算。
Future 保存異步計算的結果。可以啓動一個計算,將 Future 對象交給某個線程,然後忘掉它。Future 對象的所有者在結果計算好之後就可以獲得它。

public interface Future<V>
{
	V get() throws . .
	V get(long timeout, TimeUnit unit) throws . .
	void cancel(boolean maylnterrupt);
	boolean isCancelled();
	boolean isDone();
}

第一個 get 方法的調用被阻塞, 直到計算完成。如果在計算完成之前, 第二個方法的調用超時,拋出一個 TimeoutException 異常。如果運行該計算的線程被中斷,兩個方法都將拋出 IntermptedException。如果計算已經完成, 那麼 get 方法立即返回。
如果計算還在進行,isDone 方法返回 false; 如果完成了, 則返回 true。
可以用 cancel 方法取消該計算。如果計算還沒有開始,它被取消且不再開始。如果計算處於運行之中,那麼如果 maylnterrupt 參數爲 true, 它就被中斷。
FutureTask 包裝器是一種非常便利的機制, 可將 Callable轉換成 Future 和 Runnable,它同時實現二者的接口。例如:

Callable<Integer> nyComputation = . . .;
FutureTask<Integer> task = new FutureTask<Integer>(myConiputation);
Thread t = new Thread(task); // it's a Runnable
t.start();
Integer result = task.get();// it's a Future

對 get 的調用會發生阻塞, 直到有可獲得的結果爲止。

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