java併發編程——併發容器和併發工具介紹

        java.util.concurrent包下面爲咱們提供了豐富的類和接口供咱們開發出支持高併發、線程安全的程序。下面將從三個方面對這些基礎構建類作以介紹和總結。java

同步容器類,介紹Vector,HashTable和Collections.SynchronizedXXX();數組

併發容器類,介紹ConcurrentHashMap,CopyOnWrite容器以及阻塞隊列。安全

併發工具類,介紹CountLatch,FututeTask,Semaphore和CyclicBarrier。多線程

  • 第一部分:同步容器類

     同步容器類包括:Vector和HashTable,這兩個類是早期JDK的一部分。此外還包括了JDK1.2以後提供的同步封裝器方法Collections.synchronizedXxx()。併發

  • 同步容器類的問題

咱們都知道Vector是線程安全的容器,但當咱們對Vector經行復合操做時每每會獲得意想以外的結果。好比迭代操做:異步

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

       例如兩個線程A和B,A線程對容器進行迭代操做的時候B線程可能對容器進行了刪除操做,這樣就會致使A線程的迭代操做拋出IndexOutOfBoundsException。而這顯然不是咱們想獲得的結果,因此,爲了解決不可靠迭代的問題,咱們只能犧牲伸縮性爲容器加鎖,代碼以下:函數

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

       加鎖防止了迭代期間對容器的修改,但這同時也下降了容器的併發性。當容器很大時,其餘線程要一直等待迭代結束,所以性能不高。高併發

  • 迭代器與CoucurrentModificationException

       上面的例子咱們舉了Vector這種「古老」的容器。可是實際上不少「現代」的容器類也並無避免上面所提到的問題。好比:for-each循環和Iterator。當咱們調用Iterator類的hasNext和next方法對容器進行迭代時,若其餘線程併發修改該容器,那麼此時容器表現出的行爲是「及時失敗」的,即拋出CoucurrentModificationException。所以,爲了不以上問題,咱們又不得不爲容器加鎖,或者對容器進行「克隆」,在副本上進行操做,但這樣作的性能顯然是不高的。工具

  • 影藏的迭代器

       這裏要特別提到的是有些類的方法會隱藏的調用容器的迭代器,這每每是很容易被咱們忽視的。看下面列子:性能

private final Set<Integer> set = new HashSet<Integer>();

public synchronized void add(Integer i){set.add(i);}

public void addMore(){
    for(int i=0;i<10;i++)
        add(i);
    System.out.println("this is set:"+set);
}

       以上代碼能看出對容器的迭代操做嗎?乍看沒有。但實際上最後一段「this is set:」+set的代碼影藏的調用了set的toString方法,而toString方法又會影藏的調用容器的迭代方法,這樣該類的addMore方法一樣可能會拋出CoucurrentModificationException異常。更嚴重的問題是該類並非線程安全的,咱們可使用SynchronizedSet來包裝set類,對同步代碼進行封裝,這樣就避免了問題。此外咱們還須要特別注意:容器的equals、hashCode方法都會隱式的進行迭代操做,當一個容器做爲另外一個容器的健值或者元素時就會出現這種狀況。一樣,containsAll,removeAll等方法以及把容器當作參數的構造函數,都會進行迭代操做。全部這些間接迭代的操做均可能致使程序拋出CoucurrentModificationException。

  • 第二部分:併發容器類

       java.util.concurrent包下面爲咱們提供了豐富的高併發容器類。經過併發容器來替換同步容器,能夠極大地提升伸縮性和下降風險。這些容器按照不一樣的用途分爲如下幾類:

  • ConcurrentHashMap

       Java 5.0增長的ConcurrentHashMap幾乎是最經常使用的併發容器了。與·HashTable相比,ConcurrentHashMap不只線程安全,並且還支持高併發、高吞吐。ConcurrentHashMap的底層實現使用了分段鎖技術,而不是獨佔鎖,它容許多個線程能夠同時訪問和修改容器,而不會拋出CoucurrentModificationException異常,極大地提升了效率。在這裏要說明的是ConcurrentHashMap返回的迭代器是弱一致性的,而不是及時失敗的。另外size、isEmpty等須要在整個容器上執行的方法其返回值也是弱一致性的,是近似值而非準確值。因此,在實際使用中要對此作權衡。與同步容器和加鎖機制相比,ConcurrentHashMap優點明顯,是咱們優先考慮的容器。

  • CopyOnWriteArrayList

       CopyOnWrite容器用於替代同步的List,它提供了更好的併發性,而且在使用時不須要加鎖或者拷貝容器。CopyOnWriteArrayList的主要應用就是迭代容器操做多而修改少的場景,迭代時也不會拋出CoucurrentModificationException異常。CopyOnWrite容器的底層實現是在迭代操做前都會複製一個底層數組,這保證了多線程下的併發性和一致性,可是當底層數組的數據量比較大的時候,就須要效率問題了。

  • 阻塞隊列和生產者—消費者模型

       Java 5.0以後新增長了Queue(隊列)和BlockingQueue(阻塞隊列)。Queue的底層實現其實就是一個LinkedList。隊列是典型的FIFO先進先出的實現。阻塞隊列提供了不少現成的方法能夠知足咱們實現生產者消費者模型。

      生產者—消費者模型簡單理解就是一個緩衝容器,協調生產者和消費者之間的關係。生產者生產數據扔到容器裏,消費者直接從容器裏消費數據,你們不須要關心彼此,只須要和容器打交道,這樣就實現了生產者和消費者的解耦。

       隊列分爲有界隊列和無界隊列,無界隊列會由於數據的累計形成內存溢出,使用時要當心。阻塞隊列有不少種實現,最經常使用的有ArrayBlockingQueue和LinkedBlockingQueue。阻塞隊列提供了阻塞的take和put方法,若是隊列已滿,那麼put方法將等待隊列有空間時在執行插入操做;若是隊列爲空,那麼take方法將一直阻塞直到有元素可取。有界隊列是一個強大的資源管理器,它能抑制產生過多的工做項,使程序更加健壯。

  • 其餘併發容器類

       除了以上較經常使用的併發容器外,Java還爲咱們提供了一些個性化的容器類以知足咱們的需求。BlockingDeque,PriorityBlockingQueue,DelayQueue和SynchronousQueue。

Deque是一種雙端隊列,它支持在兩端進行插入和刪除元素,Deque繼承自Queue。BlockingDeque就是支持阻塞的雙端隊列,經常使用的實現有LinkedBlockingDeque。雙端隊列最典型的應用是工做密取,每一個消費者都有各自的雙端隊列,它適用於既是生產者又是消費者的場景。

PriorityBlockingQueue是一個能夠按照優先級排序的阻塞隊列,當你但願按照某種順序來排序時很是有用。

DelayQueue是一個無界阻塞隊列,只有在延遲期滿時才能從中提取元素。

SynchronousQueue實際上不能算做一個隊列,他不提供元素的存儲功能,它只是維護一組線程,這些線程只是 等待將元素加入或者移除隊列。SynchronousQueue至關於扮演一個直接交付的角色,所以它的put和get方法是一直阻塞的,有更少的延遲。

  • 第三部分:併發工具類

      若是遇到以上併發容器類沒法解決的問題,大多數狀況下咱們可使用併發工具類來解決問題。全部的同步工具類都包含共同的屬性:「他們封裝了一些狀態,這些狀態將決定執行併發工具類的線程是繼續執行仍是等待,此外還提供了一些方法對狀態進行操做,以及另一些用於高效的等待併發工具類進入到預期狀態。」併發工具類主要包括如下幾類:

  • CountDownLatch

       中文翻譯爲「閉鎖」,Java API上是這樣描述的:一個同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待。咱們能夠這樣理解:CountDownLatch就至關於一扇門,當N我的都到齊以後才能夠打開這扇門。門上面有一個計數器,用於記錄打開門還須要的人數,好比須要五人,初始化計數器就顯示五人,來了兩人之後,計數器就變成了三,當五我的都到齊以後計數器變爲零,此時門就打開了,全部人均可以進入了。看如下示例,建立必定數量的線程,用它們併發的執行某個指定任務。

public class CountDownLatchTask {
	public long timeTask(int nThreads,final Runnable task) 
			throws InterruptedException{
		final CountDownLatch startTask = new CountDownLatch(1);
		final CountDownLatch endTask = new CountDownLatch(nThreads);
		
		for(int i=0;i<nThreads;i++){
			Thread t = new Thread(){
				public void run(){
					try{
						startTask.await();
						try{
							task.run();
						}finally{
							endTask.countDown();
						}
					}catch(InterruptedException e){
						
					}
				}
			};
			t.start();
		}
		
		long start = System.currentTimeMillis();
		startTask.countDown();
		endTask.await();
		long end = System.currentTimeMillis();
		return end-start;
	}
}
  • FutureTask

        FutureTask表示一個能夠取消的異步計算。FutureTask表示的計算是經過Callable實現的,Callable與Runnable的區別就是Callable能夠返回值或者拋出異常。因此咱們能夠用它來表示一些時間較長的計算,這些計算能夠在使用計算結果以前啓動,從而減小等待的時間。

  • Semaphorey

        信號量用來控制同時訪問某個資源的數量,或者同時執行某個操做的數量。因此信號量的典型應用就是用來實現某種資源池,或者對容器施加邊界。看下面代碼實例,爲一個Set設置邊界:

public class SemaphoreSet<T> {
	private final Set<T> set;
	private final Semaphore sem;
	
	public SemaphoreSet(int bounds){
		set = Collections.synchronizedSet(new HashSet<T>());
		sem = new Semaphore(bounds);
	}
	
	public boolean add(T o) throws InterruptedException{
		sem.acquire();
		boolean isSuccess = false;
		try{
			isSuccess = set.add(o);
			return isSuccess;
		}finally{
			if(!isSuccess)
				sem.release();
		}
	}
	
	public boolean remove(T o) throws InterruptedException{
		sem.acquire();
		boolean isSuccess = false;
		try{
			isSuccess = set.remove(o);
			return isSuccess;
		}finally{
			if(!isSuccess)
				sem.release();
		}
	}
}
  • CyclicBarrier

        柵欄相似於閉鎖。它們的區別是:閉鎖是一次性對象,一旦達到終止狀態就不能被重置,而柵欄相似於水壩,當達到某個水位線之後開閘放水,放水完畢後又能夠被重置。閉鎖與柵欄的另外一個區別體如今:全部線程必須都達到柵欄位置,才能繼續執行。閉鎖等待事件,而柵欄等待線程。

綜上所述,經過對同步容器、併發容器和併發工具的介紹,你們大體瞭解了每種容器的使用場景以及各自的侷限。併發容器和併發工具的出現極大地提升了程序的吞吐量和併發性,是大型網站開發過程當中的必不可少的工具。

相關文章
相關標籤/搜索