同步阻塞
每個Java對象有一個鎖。線程可以通過同步方法獲得鎖。還有通過進入一個同步阻塞獲得鎖。當線程進入如下形式的阻塞:
// 這是同步塊的語法
synchronized (obj) {}
於是它獲得obj的鎖。
有時會發現“特殊的”鎖:
public class Bank {
...
private Object lock = new Object();
...
public void transfer(int from, int to, int amount) {
synchronized(lock){ //臨時鎖
accounts[from] -= amount;
accounts[to] += amount;
}
}
...
}
在此,lock對象被創建僅僅是用來使用每個Java對象持有的鎖。
有時使用一個對象鎖來實現額外的原子操作,實際上稱爲客戶端鎖定(client-side locking)。Vector< Double >存儲銀行餘額,它的方法是同步的:
public void transfer(Vector<Double> accounts, int from, int to, int amount) {
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
System.out.println(...);
}
Vector類的get和set方法都是同步的,但並沒有什麼幫助。第一次對get調用完成之後,完全可能在transfer方法中被剝奪運行權。
可以截獲這個鎖:
public void transfer(Vector<Double> accounts, int from, int to, int amount) {
synchronized(accounts) {
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
System.out.println(...);
}
}
這個方法可以工作,但完全依賴於Vector類對自己的所有可修改方法都是用內部鎖(Vector類的文檔沒有給出這樣的承諾)。客戶端鎖定是非常脆弱的,不推薦使用。
監視器概念
監視器(monitor)不需要考慮如何加鎖的情況下,就可以保證多線程的安全性。
用Java術語來講,監視器具有以下特性:
1.監視器是隻包含私有域的類
2.每個監視器類的對象有一個相關的鎖
3.使用該鎖對所有的方法進行加鎖
4.該鎖可以有任意多個相關條件
Java設計者以不是很精確的方式採用了監視器概念,Java中的每一個對象有一個內部的鎖和內部的條件。如果一個方法用synchronized關鍵字聲明,那麼它表現的就像一個監視器方法。通過調用wait/notify/notifyAll來訪問條件變量。
在3個方面Java對象不同於監視器,從而使得線程安全性下降:
1.域不要求必須是private
2.方法不要求必須是synchronized
3.內部鎖對客戶是可用的
Volatile域
volatile關鍵字爲實例域的同步訪問提供了一種免鎖機制。如果聲明一個域爲volatile,那麼編譯器和虛擬機就知道該域是可能被另一個線程併發更新的。
假定一個布爾標記done,它的值被一個線程設置卻別另一個線程查詢,可以使用鎖:
private boolean done;
public synchronized boolean isDone() {return done;}
public synchronized void setDone() {done = true;}
使用內部鎖,如果另一個線程已經對該對象加鎖,isDone和setDone方法可能阻塞。如果注意到這個方面,一個線程可以爲這一變量使用獨立的Lock。但是這也會帶來很多麻煩。
可以將域聲明爲bolatile:
private volatile boolean done;
public boolean isDone() {return done;}
public void setDone() {done = true;}
volatile不能提供原子性,例如done = !done,不能確保翻轉域中的值,不能保證讀取、翻轉和寫入不被中斷。
final變量
除非使用鎖或volatile修飾符,否則無法從多個線程安全地讀取一個域。
還有一種情況可以安全的訪問共享域,即這個域聲明爲final:
final Map<String, Double> accounts = new HashMap<>();
其他線程會在構造函數完成之後纔看到這個accounts變量。
當然,對這個映射表的操作並不是線程安全的,如果多個線程在讀寫這個映射表,仍然需要進行同步。
原子性
對共享數據除了賦值之外並不完成其他操作,可以將這些共享變量聲明爲volatile。
java.util.concurrent.atomic包中有很多類使用了很高效的機器級指令(不是鎖)來保證其他操作的原子性。
AtomicInteger類提供了incrementAndGet和decrementAndGet,它們分別以原子方式將一個整數自增或自減。
incrementAndGet以原子方式將AtomicLong自增,並返回自增後的值。獲得值、增1並設置然後生成新值的操作不會中斷。可以保證即使是多個線程併發訪問同一個實例,也會計算並返回爭取的值。
如果希望完成更復雜的更新,就必須使用compareAndSet,例如希望跟蹤不同線程觀察的最大值:
// 不可行的
public static AtomicLong largest = new AtomicLong();
largest.set(Math.max(largest.get(). observed));
// 上述更新不是原子的,應當在一個循環中計算新值和使用comapreAndSet
do {
oldValue = largest.get();
newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));
如果另一個線程也在更新largest,就可能阻止這個線程更新。compareAndSet會返回false,而不會設置新值。循環會更次嘗試,讀取更新後的值,並嘗試修改。最終它會成功地用新值替換原來的值。聽起來有些麻煩,不過compareAndSet會映射到一個處理器操作,比使用鎖速度更快。
Java SE 8中,不再需要編寫這樣的循環代碼,可以提供一個lambda表達式:
largest.updateAndGet(x -> Math.max(x, observed));
// 或
largset.accumulateAndGet(observed, Math::max);
accumulateAndGet方法利用一個二元操作符來合併原子值和所提供的參數。還有getAndUpdate和getAndAccumulate方法可以返回原值。
類AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpdater、AtomicLongArray、AtomicLongFieldUpdater、AtomicReference、AtomicReferenceArray和AtomicReferenceFieldUpdater也提供這些方法。
如果有大量線程要訪問原子值,性能會大幅下降,因爲樂觀更細需要太多次嘗試。Java SE 8提供了LongAdder和LongAccumulator類來解決問題。
LongAdder包含多個變量(加數),總和爲當前值。可以有多個線程更新不同的加數,線程個數增加時會自動提供新的加數。通常只有當所有工作完成之後才需要總和的值。
如果認爲可能存在大量競爭,只需要使用LongAdder而不是AtomicLong。方法名稍有區別,調用increment讓計數器自增,或者調用add增加一個量,或調用sum獲取總和。
final LongAdder adder = new LongAdder();
for (...){
pool.submit(() -> {
while(...) {
...
if(...) adder.increment();
}
});
}
...
long total = adder.sum();
LongAccumolator將這種思想推廣到任意的累加操作。在構造器中,可以提供這個操作以及它的零元素。要加入新值,調用accumulate。調用get來獲得當前值:
LongAccumulator adder = new LongAccumulator(Long::sum, 0);
adder.accumulate(value);
在內部,這個累加器包含變量a1、a2…an,每個變量初始化爲零元素。
調用accumulate並提供值v時,其中一個變量會以原子方式更新爲ai=ai op v,op是中綴形式的累加操作。
get的結果是a1 op a2 op … an。如果選擇一個不同的操作,可以計算最小值和最大值。這個操作必須滿足結合律和交換律,這說明,最終結果必須獨立於所結合的中間值的順序。
DoubleAdder和DoubleAccumulator也有類似方式。
死鎖
Java編程語言沒有任何東西可以避免或打破死鎖現象,必須仔細設計程序,以確保不會出現死鎖。
線程局部變量
有時要避免共享變量,使用ThreadLocal輔助類爲各個線程提供各自的實例。
SimpleDateFormat類不是線程安全的:
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateStamp = dateFormat.format(new Date());
兩個線程都執行,結果可能很混亂,dateFormat使用的內部數據結構可能會被併發的訪問所破壞。當然可以使用同步,但開銷很大。
爲每個線程構造一個實例:
public static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
String dateStamp = dateFormat.get().format(new Date());
在一個給定線程中首次調用get時,會調用initialValue方法,在此之後,get方法會返回屬於當前線程的那個實例。
在多個線程生成隨機數也存在類似問題。java.util.Random類是線程安全的,但如果多個線程需要等待一個共享的隨機數生成器會很低效。可以使用ThreadLocal爲各個線程提供單獨的生成器:
int random = ThreadLocalRandom.current().nextInt(upperBound);
ThreadLocalRandom.current()調用會返回特定於當前線程的Random類實例。
鎖測試與超時
線程在調用lock方法來獲得另一個線程所持有的鎖的時候,很可能阻塞。tryLock方法試圖申請一個鎖,在成功獲得鎖後返回true,否則立即返回false,而且線程可以立即離開去做任何其他事情。
if (myLock.tryLock()) {
try{...}
finally{myLock.unlock();}
} else {
...
}
可以調用tryLock,使用超時參數myLock.tryLock(100, TimeUnit.MILLISENCONDS),TimeUnit是一個枚舉類型,可以取的值包括SECONDS、MILLISECONDS、MICROSECONDS和NANOSECONDS。
lock方法不能被中斷。如果一個線程在等待獲得一個鎖時被中斷,中斷程序在獲得鎖之前一直處於阻塞狀態,如果出現死鎖,lock將無法終止。
帶有超時參數的tryLock如果在線程等待期間被中斷,將拋出InterruptedException,這是一個非常有用的特性,因爲運行程序打破死鎖。
lockInterruptibly方法等同於超時爲無限的tryLock。
在等待一個條件時,也可以提供超時:
myCondition.await(100, TimeUnit.MILLISECONDS);
如果一個線程被siganlAll或signal激活,或者超時時限已達到,或者線程被中斷,那麼await方法將返回。
如果等待的線程被中斷,await方法將拋出InterruptedException異常。如果希望出現這種情況時線程繼續等待,可以使用awaitUninterruptibly替代await。
讀/寫鎖
java.util.concurrent.locks定義的另一個鎖類,ReentrantReadWriteLock類。如果很多線程從一個數據結構讀取數據而很少線程修改的話,十分有用。在這種情況下,允許讀者線程共享訪問是合適的。但寫者線程必須是互斥訪問:
// 構造一個ReentrantReadWriteLock對象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 抽取讀鎖和寫鎖
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
// 所有獲取方法加讀鎖
public double getTotalBalance() {
readLock.lock();
try {
...
} finally {
readLock.unlock();
}
}
// 所有修改方法加寫鎖
public void transfer(...) {
writeLock.lock();
try {
...
} finally {
writeLock.unlock();
}
}
爲什麼棄用stop和suspend方法
stop方法用來終止一個線程;
suspend方法用來阻塞一個線程,直至另一個線程調用resume。共同點:都試圖控制一個給定線程的行爲。
stop、suspend、resume方法已經棄用。stop方法天生不安全,經驗證明suspend方法會經常導致死鎖。
stop方法終結所有未結束的方法,包括run方法。當線程被終止,立即釋放被它鎖住的所有對象的鎖。這會導致對象處於不一致狀態。一個賬戶想另一個賬戶轉賬過程中被終止,錢款已轉出,卻沒有轉入目標賬戶。
當線程要終止另一個線程時,無法知道什麼時候調用stop方法是安全的,因此該方法被棄用了。
suspend掛起一個持有鎖的線程,那麼該鎖在恢復之前是不可用的。如果調用suspend方法的線程試圖獲得同一個鎖,那麼程序死鎖:被掛起的線程等着被恢復,而將掛起的線程等待獲得鎖。
6.阻塞隊列
對於實際編程,應該儘可能遠離Java併發程序設計基礎的底層構建塊。使用由併發處理的專業人士實現的較高層次的結構要方便得多,安全得多。
對於許多線程問題,通過使用一個或多個隊列以優雅且安全的方式將其形式化。生產者線程向隊列插入元素,消費者線程則取出它們。使用線程可以安全地從一個線程向另一個線程傳遞數據。
銀行轉賬程序,轉賬線程將轉賬指令對象插入一個隊列中,而不是直接訪問銀行對象。另一個線程從隊列中取出指令執行轉賬。只有該線程可以訪問銀行對象內部。因此不需要同步。
當試圖向隊列添加元素而隊列已滿,或想從隊列移除元素而隊列爲空時,阻塞隊列(blocking queue)導致線程阻塞。在協調多個線程之間的合作時,工作者線程可以週期性的將中間結果存儲在阻塞隊列中。其他的工作者線程移除中間結果並進一步加以修改。隊列會自動地平衡負載。如果第一個線程集運行的比第二個慢,第二個線程集在等待結果時會阻塞。如果第一個線程集運行得快,它將等待第二個隊列集趕上來。
阻塞隊列方法分爲以下3類:
1.將隊列當作線程管理工具使用,將要用到put和take方法
2.向滿的隊列中添加或從空的隊列中移除元素時,add、remove和element操作拋出異常
3.一個多線程程序中,隊列會在任何時候空或滿,一定要使用offer、poll和peek方法替代,這些方法如果不能完成任務,這會給出錯誤提示而不會拋出異常
還有帶有超時offer方法和poll方法的變體:
boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);
嘗試在100毫秒的時間內在隊列的尾部插入一個元素。如果成功返回true;否則,達到超時時,返回false。類似地q.poll(100, TimeUnit.MILLISECONDS)。
java.util.concurrent包提供了阻塞隊列的幾個變種:
LinkedBlockingQueue在默認情況下容量是沒有上邊界的,也可以選擇指定最大容量;
LinkedBlockingDeque是一個上端版本;
ArrayBlockingQueue在構造時需要指定容量,並且有一個可選的參數來指定是否需要公平性。若設置了公平參數,則那麼等待了最長時間的線程會優先得到處理(通常公平性會降低性能)。
PriorityBlockingQueue是一個帶優先級的隊列,而不是先進先出隊列。元素按照優先級順序被移除。該隊列沒有容量上限。但如果隊列爲空,取元素的操作會阻塞。
DelayQueue包含實現Delayed接口的對象:
interface Delayed extends Comparable<Delayed>{
long getDelay(TimeUnit unit);
}
getDelay方法返回對象的殘留延遲。負值表示延遲已經結束。元素只能在延遲用完的情況下才能從DelayQueue移除。還必須實現comparaTo方法。DelayQueue使用該方法對元素進行排序。
Java SE 7增加了一個TransferQueue接口,允許生產者線程等待,知道消費者準備就緒可以接收一個元素。如果生產者調用q.transfer(item),這個調用會阻塞,直到另一個線程將元素item刪除。LinkedTransferQueue類實現了這個接口。
public class BlockingQueueTest {
private static final int EILE_QUEUE_SIZE = 10;
private static final int SEARCH_THREADS = 100;
private static final File DUMMY = new File("");
private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(EILE_QUEUE_SIZE);
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.print("Enter base directory (e.g. /opt/jdk1.8.0/src):");
String directory = in.nextLine();
System.out.print("Enter keyword (e.g. volatile):");
String keyword = in.nextLine();
Runnable enumerator = () -> {
try {
enumerate(new File(directory));
queue.put(DUMMY);
}catch (InterruptedException e){}
};
new Thread(enumerator).start();
for (int i = 1; i <= SEARCH_THREADS; i++) {
Runnable searcher = () -> {
try {
boolean done = false;
while (!done) {
File file = queue.take();
if (file == DUMMY) {
queue.put(file);
done = true;
} else {
search(file, keyword);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {}
};
new Thread(searcher).start();
}
}
}
public static void enumerate(File directory) throws InterruptedException {
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
enumerate(file);
} else {
queue.put(file);
}
}
}
public static void search(File file, String keyword) throws IOException {
try (Scanner in = new Scanner(file, "UTF-8")) {
int lineNumber = 0;
while (in.hasNextLine()) {
lineNumber++;
String line = in.nextLine();
if (line.contains(keyword)) {
System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber, line);
}
}
}
}
}
生產者線程枚舉在所有子目錄下的所有文件並把它們放到一個阻塞隊列中。
啓動大量搜索線程。每個線程從隊列中取一個文件,打開它,打印所有包含該關鍵字的行,然後取出下一個文件。
爲了發出完成信號,枚舉線程放置一個虛擬對象到隊列中(向行禮輸送帶上當一個寫着最後一個包的虛擬包)。當搜索線程取到這個虛擬對象時,將其放回並終止。
不需要顯示的線程同步,這個程序使用隊列數據結構作爲一種同步機制。
線程安全的集合
高效的映射、集和隊列
java.util.concurrent包提供了映射、有序集和隊列的高效實現:concurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
這些集合使用了複雜的算法,通過允許併發地訪問數據結構的不同部分來使競爭極小化。
size通常不必在常量時間內操作,確定集合當前大小通常需要遍歷。有些映射過於龐大(20億),JavaSE8引入了mappingCount方法可以把大小做爲long返回。
集合返回弱一致性(weakly consistent)的迭代器,意味着迭代器不一定能反映出它們被構造之後的所有修改,但它們不會將同一個值返回兩次,也不會拋出ConcurrentModificationException異常(java.util包的迭代器會拋出)。
併發的散列映射表,可高效地支持大量讀者和一定量寫者。默認情況下,可以有多達16個寫者線程同時執行。可以有更多的寫者線程,但如果多於16個,其他線程將暫時被阻塞,可以指定更大數目的構造器,然而沒必要。
映射條目的原子更新
ConcurrentHashMap原來的版本沒有很多方法實現原子更新,舉例多個線程會遇到單詞,想統計它們的頻率:
傳統做法是使用replace操作,它會以原子方法用一個新值替換原值,前提是之前沒有其他線程把原值替換爲其他值,必須一直這麼做,知道replace成功:
do {
oldValue = map.get(word);
newValue = oldValue == null ? 1 : oldValue + 1;
} while(!map.relace(word, oldValue, newValue));
或使用ConcurrentHashMap< String, AtomicLong >,或者Java SE8可以使用ConcurrentHashMap< String, LongAdder >:
map.putIfAbsent(word, new LongAdder());
map.get(word).increment();
Java SE 8提供了compute方法,可以提供一個鍵和一個計算新值的函數。這個函數接收鍵和相關聯的值,會計算新值:
map.compute(word, (k, v) -> v ==null ? 1 : v + 1);
ConcurrentHashMap不允許有null值,很多方法都是用null來指示映射中某個給定的鍵不存在。
另外還有computeIfPresent和computeIfAbsent方法,分別只在已有原值情況下計算新值,或者只有沒有原值情況下計算新值。
map.computeIfAbsent(word, k -> new LongAdder()).increment();
首次添加一個鍵時通常需要特殊處理,利用merge方法可以方便的處理,這個方法有一個參數表示鍵不存在時使用的初始值。否則,就會調用提供的函數來結合原值與初始值(compute不處理鍵):
map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);
// 或
map.merge(word, 1L, Long::sum);
對併發散列映射的批操作
Java SE 8爲併發散列映射提供了批操作,即使有其他線程在處理映射,也能安全地執行。
有3種不同的操作:
1.搜索(search)爲每個鍵或值提供一個函數,直到函數生成一個非null的結果。然後搜索終止,返回這個函數的結果。
2.歸約(reduce)組合所有鍵或值,這裏使用所提供的一個累加函數
3.forEach爲所有鍵或值提供一個函數
每個操作都有4個版本:
1.operationKeys:處理鍵
2.operationValues:處理值
3.operation:處理鍵和值
4.operationEntries:處理Map.Entry對象
需要指定一個參數化閾值(parallelism threshold)。如果包含的元素多於這個閾值,就會並行完成。如果希望批操作在一個線程中運行,可以使用閾值Long.MAX_VALUE。如果希望儘可能多的線程運行,閾值可以爲1。
search的方法:
U searchKeys(long threshold, BiFunction< ? super K, ? extends U > f)
U searchValues(long threshold, BiFunction< ? super K, ? extends U > f)
U search(long threshold, BiFunction< ? super K, ? extends U > f)
U searchEntries(long threshold, BiFunction< ? super K, ? extends U > f)
希望找出一個第一次出現次數超過1000次的單詞。需要搜索鍵和值:
String result = map.search(threshold, (k, v) -> v > 1000 ? k : null);
forEach方法有兩種形式:
// 1.爲各個映射條目提供一個消費者函數
map.forEach(threshold, (k, v) -> System.out.println(k + " -> " + v));
// 2.有一個轉換器函數,要先提供,其結果會傳遞到消費者
map.forEach(threshold, (k, v) -> k + " -> " + v, System.out::println);
轉換器可以用作爲一個過濾器,只要轉換器返回null,這個值就會被悄無聲息地跳過。例如,下面只打印有大值的條目:
map.forEach(threshold, (k, v) -> v > 1000 ? k + " -> " + v : null, System.out::println);
reduce操作用一個累加函數組合其輸入。例如,可以計算所有值的總和:
Long sum = map.reduceValues(threshold, Long::sum);
// 與forEach類似,也可以提供一個轉換器函數,計算最長的鍵的長度:
Integer maxlength = map.reduceKeys(threshold, String::length, Integer::max);
轉換器可以作爲一個過濾器,通過返回null來排除不想要的輸入。
統計多少個條目的值>1000:
Long count = map.reduceValues(threshold, v -> v > 1000 ? 1L : null, Long::sum);
對於int、long、double輸出還有特殊化操作,分別有後綴ToInt、ToLong和ToDouble。需要把輸入轉換爲一個基本類型值,並指定一個默認值和一個累加器函數。映射爲空時返回默認值。
long sum = map.reduceValuesToLong(threshold, Long::longValue, 0, Long::sum);
併發集視圖
並沒有一個線程安全的集,ConcurrentHashMap的靜態newKeySet方法會生成一個Set< K >,實際上是ConcurrentHashMap< K, Boolean>的包裝器(所有的映射值都是Boolean.TRUE)。
Set<String> words = ConcurrentHashMap.<String>newKeySet();
如果刪除這個集的元素,這個鍵會從映射中刪除。不能向鍵集增加元素,因爲沒有相應的值可以增加。
Java SE 8增加了第二個keySet方法,包含一個默認值,可以在爲集增加元素時使用:
Set<String> words = map.keySet(1L);
words.add("Java");
// 如果Java在words中不存在,現在它會有一個值1
寫數組的拷貝
CopyOnWriteArrayList和CopyOnWriteArraySet是線程安全的集合,其中所有的修改線程對底層數組進行復制。如果在集合上進行迭代的線程數超過修改線程數,這樣的安排是有用的。當構建一個迭代器的時候,它包含一個對當前數組的引用。如果數組後來被修改了,迭代器仍然引用舊數組,但是集合的數組已經被替換了。因而,舊的迭代器擁有一致的視圖,訪問它無須任何同步開銷。
並行數組算法
Arrays提供了大量並行化操作。靜態Arrays.parallelSort方法可以對一個基本類型或對象的數組進行排序:
String contents = new String(Files.readAllBytes(Paths.get("alice.txt")), StandardCharsets.UTF_8);
String[] words = contents.split("[\\P{L}]+");
Arrays.parallelSort(words);
// 對對象排序時,可以提供Comparator
Arrays.parallelSort(words, Comparator.comparing(String::length));
// 對於所有方法都可以提供一個範圍的邊界
values.parallelSort(values.length / 2, values.length);
API設計者希望通過parallel方法名指出排序是並行化的,用戶就會注意避免使用有副作用的比較器。
parallelSetAll方法會用由一個函數計算得到的值填充另一個數組。這個函數接收元素索引,然後計算相應位置上的值:
Arrays.parallelSetAll(values, i -> i % 10);
// 0 1 2 3 4 5 6 7 8 9 0 1 2...
parallelPrefix方法,會用對應一個給定結合操作的前綴的累加結果替換各個數組元素。考慮數組[1,2,3,4,…]和×操作。
Arrays.parallelPrefix(values, (x, y) -> x * y);
// 函數包含
[1, 1*2, 1*2*3, 1*2*3*4,...]
可能很奇怪,不過這個計算確實可以並行化。可以在不同的數組區中並行完成計算。log(n)步之後完成,如果有足夠多的處理器,這會遠遠勝過直接的線性計算。
較早的線程安全集合
Vector和Hashtable類是Java初始版本提供的線程安全的動態數組和散列表,這些類已經被棄用。取而代之的是ArrayList和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()) {...}
}
如果使用for each循環必須使用同樣的代碼,因爲循環使用了迭代器。如果在迭代過程中,別的線程修改了集合,迭代器會失敗,拋出ConcurrentModificationException異常。同步仍然是需要的,因此併發的修改可以被可靠地檢測出來。
最好使用java.util.concurrent包中定義的集合,不要使用同步包裝器。有一個例外是經常被修改的數組列表,同步的ArrayList可以勝過CopyOnWriteArrayList。