java併發編程-構建塊

java併發編程-Executor框架

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併發編程實踐 》。

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