java5引入了很多新的併發容器和工具,極大的簡化了併發程序的編寫。本文先介紹Collections.synchronizedXXX工廠方法創建的同步容器的不足,再介紹ConcurrentHashMap,CopyOnWriterArrayList,BlockingQueue,CountDownLatch,Semaphore,CyclicBarrier和顯示鎖類。
一、引言
所有的併發問題都來源於如何協調訪問併發狀態,可變狀態越少,併發控制也就越容易容易。沒有可變狀態的對象永遠是現成安全。對有可變狀態的對象的併發訪問必須進行加鎖,對每個方法都加鎖並不能保證併發訪問一定處於一致的狀態。在構建併發程序的時候,可以把併發的控制代理到已有的併發類,比如類庫提供的併發容器或工具。
二、同步容器的不足
對Collections.synchronizedXXX工廠方法創建的同步容器,每個方法都是使用該容器的內部鎖進行控制的,這會帶來一性能問題,因爲多個安全的讀操作也要等待排它鎖。即使每個方法的調用都是線程安全的,但同時調用多個操作室,不一定是線程安全的,比如缺少即添加,相等即修改等炒作,是兩步原子操作構成的,和在一起並不是原子的,必須在調用代碼裏使用容器的鎖進行控制。另外在迭代集合的時候,還會由於併發修改而拋出ConcurrentModefiedExceptioin,這往往是調用程序不希望的結果。
三、ConcurrentHashMap,CopyOnWriterArrayList
ConcurrentHashMap使用的內部細粒度的分離鎖,這個鎖機制允許任意數量的讀線程併發訪問,提高了吞吐率。在迭代時不會拋出ConcurrentModefiedExceptioin,如果在迭代期間有修改發生,返回的是迭代開始時的狀態。另外對缺少即添加,相等即修改等二元操作也有相應的方法支持。ConcurrentHashMap實現了ConcurrentMap提供的幾個特殊原子操作:
public V putIfAbsent(K key, V value)
如果指定鍵已經不再與某個值相關聯,則將它與給定值關聯。
public boolean remove(Object key, Object value)
只有目前將鍵的條目映射到給定值時,才移除該鍵的條目。
public boolean replace(K key, V oldValue, V newValue)
只有目前將鍵的條目映射到給定值時,才替換該鍵的條目。
public V replace(K key, V value)
只有目前將鍵的條目映射到某一值時,才替換該鍵的條目。
CopyOnWriterArrayList 是ArrayList的一個併發替代品,通常情況下,它提供比較好的併發性,允許多個現在併發的對其進行迭代。每次需要修改時,便會創建並重新發佈一個新的容器拷貝,以此來實現可變性。因爲底層使用數組實現,如果數組元素較多是,複製多付出的代價較大。
三、BlockingQueue
阻塞隊列提供了可以阻塞的put和take方法,以及與之等價的可以指定超時的offer和poll。如果Queue是空的,那麼take方法會一直阻塞,直到有元素可用。如果Queue是有線長度的,隊列滿的時候put方法也會阻塞。BlockingQueue可以很好的支持生產者和消費者模式,生產者往隊列裏put,消費者從隊列裏get,兩者能夠得好很好的同步。BlockingQueue的實現類LinkedBlockingQueue和ArrayBlockingQueue是FIFO隊列,PriorityBlockingQueue是一個按優先級排序的隊列。使用BlockingQueue構建的一個生產者與消費例子:
消費者:
public class Consumer implements Runnable {
private BlockingQueue<Food> queue;
private ExecutorService exec;
public Consumer(BlockingQueue<Food> queue, ExecutorService exec) {
this.queue = queue;
this.exec = exec;
}
@Override
public void run() {
while (!exec.isShutdown()) {
try {
Thread.sleep(2000);
Food food = queue.take();
System.out.println("Consumer " + food);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (RejectedExecutionException e) {
}
}
}
}
生產者:
public class Producer implements Runnable {
private BlockingQueue<Food> queue;
private ExecutorService exec;
public Producer(BlockingQueue<Food> queue, ExecutorService exec) {
this.queue = queue;
this.exec = exec;
}
@Override
public void run() {
while (!exec.isShutdown()) {
Food food = new Food();
try {
Thread.sleep(4000);
queue.put(food);
System.out.println("Produce " + food);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (RejectedExecutionException e) {
}
}
}
}
Main:
BlockingQueue<Food> queue = new ArrayBlockingQueue<Food>(5);
ExecutorService exec = Executors.newFixedThreadPool(3);
Producer p1 = new Producer(queue, exec);
Producer p2 = new Producer(queue, exec);
Consumer c1 = new Consumer(queue, exec);
exec.execute(p1);
exec.execute(p2);
exec.execute(c1);
try {
Thread.sleep(10000);
} catch (InterruptedException ignored) {
}
exec.shutdown();
四、CountDownLatch
閉鎖(Latch),它可以延遲線程的進度知道線程到達終止狀態。一個閉鎖工作方式就像一道門,直到閉鎖到達終點狀態之前,門一直關閉着。終點狀態到了之後,所有阻塞的線程都可以通過。CountDownLatch 使用一個計數器作爲終點狀態,知道計數器的值到達0時,閉鎖纔會打開。調用await 方法,線程會阻塞知道計數器爲0,countDown 方法使計數器減一。
閉鎖有兩種常見的用法,開始閉鎖,結束閉鎖。開始閉鎖用於等待一個條件到達後所有線程一起執行,結束閉鎖可以用來等待所有條件或所有線程結束後再進行後續處理。例子:
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch endLatch = new CountDownLatch(3);
Runnable prepare = new Runnable() {
@Override
public void run() {
try {
startLatch.await();//等待開始閉鎖,線程同時開始執行
System.out.println("收拾東西,準備出門");
Random rnd = new Random();
Thread.sleep(rnd.nextInt(1000));
} catch (InterruptedException ignored) {
}
endLatch.countDown();
}
};
Thread mum = new Thread(prepare);
Thread dad = new Thread(prepare);
Thread me = new Thread(prepare);
mum.start();
dad.start();
me.start();
startLatch.countDown();
try {
endLatch.await();
} catch (InterruptedException ignored) {
}
System.out.println("逛街");
五、Semaphore,信號量
使用信號量進行同步和互斥的控制是最經典的併發模型,java中也提高支持。一個Semaphore管理一個有效的許可 集,許可基的數量通過構造函數傳入,通過acquire方法申請一個許可,許可數爲0則阻塞線程,否則許可數減一,使用release方法釋放一個許個,許可數加一。一個技術量爲1的Semaphore爲二元信號量,相當於一個互斥鎖,表示不可重入的鎖。一個使用信號量控制併發容器上屆的例子:
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}
六、CyclicBarrier
關卡(Barrier)類似於閉鎖,他們都能阻塞一組線程,知道某些事件發生,不同之處在於所有CyclicBarrier等待的是現線程,只有一定數目的線程到達這個點時,才允許同時通過。它允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)。在涉及一組固定大小的線程的程序中,這些線程必須不時地互相等待,此時 CyclicBarrier 很有用。因爲該 barrier 在釋放等待線程後可以重用,所以稱它爲循環 的 barrier。CyclicBarrier 支持一個可選的 Runnable 命令,在一組線程中的最後一個線程到達之後(但在釋放所有線程之前),該命令只在每個屏障點運行一次。若在繼續所有參與線程之前更新共享狀態,此屏障操作很有用。
public class Main {
public static CyclicBarrier getCyclicBarrier(int count) {
if (count <= 0)
return null;
final CyclicBarrier cyclicBarrier = new CyclicBarrier(count,
new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("conditon is arrive and CycleBarrier is running");
}
});
return cyclicBarrier;
}
public static Thread getThread(String nameOfThread,
final CyclicBarrier cyclicBarrier) {
Thread thread = new Thread(nameOfThread) {
public void run() {
System.out.println(this.getName() +
"is begin; and count is "+ (++count));
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(this.getName() + "finished");
}
};
return thread;
}
static int count = 0;
public static void main(String[] args) {
/** define a cyclicBarrier and number of barrier is 2. */
CyclicBarrier cyclicBarrier = getCyclicBarrier(2);
Thread threadOne = getThread("threadOne", cyclicBarrier);
threadOne.start();
Thread threadTwo = getThread("threadTwo", cyclicBarrier);
threadTwo.start();
}
}
該例子中CyclicBarrier等待兩個線程到達後輸出conditon is arrive and CycleBarrier is running,兩個線程都從await中返回。
七、顯式鎖
在java 5之前,用於調節共享對象訪問的機制只有synchronized和volatile。java 5提供了新的選擇:ReentrantLock。ReentrantLock能夠提供更多的高級特性,比如輪詢和可定時的加鎖,可中斷的加鎖。以及一個支持讀鎖和寫鎖的ReentrantReadWriteLock。使用ReentrantLock必須手動使用lock或其他操作加鎖,在finally塊中unlock。
ReentrantLock:一個可重入的互斥鎖Lock,它具有與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。 使用ReentrantLock構建的同步Map:
public class LockedMap<K, V> {
private Map<K, V> map;
private Lock lock = new ReentrantLock();
public LockedMap(Map<K, V> map) {
this.map = map;
}
public V get(K key) {
lock.lock();
try {
return map.get(key);
} finally {
lock.unlock();
}
}
public void put(K key, V value) {
lock.lock();
try {
map.put(key, value);
} finally {
lock.unlock();
}
}
}
public class ReentrantLockTest {
private List<Integer> numbers = new ArrayList<Integer>();
private Lock numbersLock = new ReentrantLock();
public void addNumbers(int num) {
try {
numbersLock.lock();
numbers.add(num);
} finally {
numbersLock.unlock();
}
}
public void outputNumbers() {
try {
if (numbersLock.tryLock(1, TimeUnit.SECONDS)) {
for (int num : numbers) {
System.out.println(num);
}
}
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
numbersLock.unlock();
}
}
public static void main(String[] args) {
final ReentrantLockTest test = new ReentrantLockTest();
Executor pool = Executors.newFixedThreadPool(3);
pool.execute(new Runnable() {
public void run() {
Random rnd = new Random();
while (true) {
int number = rnd.nextInt();
test.addNumbers(number);
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
}
});
pool.execute(new Runnable() {
public void run() {
while (true) {
test.outputNumbers();
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
}
}
});
}
}
ReentrantReadWriteLock提供了對讀鎖和寫鎖的支持,同一時刻,可允許多個讀鎖,但只允許有一個寫鎖,讀鎖的獲取和寫鎖的獲取是互斥的。從ReentrantReadWriteLock對象的readLock方法可以獲得相應的讀鎖,writeLock方法可以獲得相應的寫鎖。使用ReentrantReadWriteLock構建的Map,允許多個get操作併發執行:
public class ReadWriteMap<K,V> {
private Map<K,V> map;
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
public ReadWriteMap(Map<K,V> map){
this.map = map;
}
public V get(K key){
readLock.lock();
try{
return map.get(key);
}
finally{
readLock.unlock();
}
}
public void put(K key,V value){
writeLock.lock();
try{
map.put(key, value);
}
finally{
writeLock.unlock();
}
}
}
所有代碼見附件。本文參考《Java併發編程實踐 》。