併發編程(四)——JUC併發包常用方法介紹

併發包

(計數器)CountDownLatch

      CountDownLatch 類位於java.util.concurrent包下,利用它可以實現類似計數器的功能。比如有一個任務A,它要等待其他4個任務執行完畢之後才能執行,此時就可以利用CountDownLatch來實現這種功能了。CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量。每當一個線程完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務。

public static void main(String[] args) throws InterruptedException {
	CountDownLatch countDownLatch = new CountDownLatch(2);
	new Thread(new Runnable() {

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + ",子線程開始執行...");
			countDownLatch.countDown();
			System.out.println(Thread.currentThread().getName() + ",子線程結束執行...");
		}
	}).start();
	
	new Thread(new Runnable() {

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + ",子線程開始執行...");
			countDownLatch.countDown();//計數器值每次減去1
			System.out.println(Thread.currentThread().getName() + ",子線程結束執行...");
		}
	}).start();
	countDownLatch.await();// 減去爲0,恢復任務繼續執行
    System.out.println("兩個子線程執行完畢....");
    System.out.println("主線程繼續執行.....");
    for (int i = 0; i <10; i++) {
		System.out.println("main,i:"+i);
	}
}

(屏障)CyclicBarrier

      CyclicBarrier初始化時規定一個數目,然後計算調用了CyclicBarrier.await()進入等待的線程數。當線程數達到了這個數目時,所有進入等待狀態的線程被喚醒並繼續。

      CyclicBarrier就象它名字的意思一樣,可看成是個障礙, 所有的線程必須到齊後才能一起通過這個障礙。

      CyclicBarrier初始時還可帶一個Runnable的參數, 此Runnable任務在CyclicBarrier的數目達到後,所有其它線程被喚醒前被執行。

class Writer extends Thread {
	private CyclicBarrier cyclicBarrier;
	public Writer(CyclicBarrier cyclicBarrier){
		 this.cyclicBarrier=cyclicBarrier;
	}
	@Override
	public void run() {
		System.out.println("線程" + Thread.currentThread().getName() + ",正在寫入數據");
		try {
			Thread.sleep(3000);
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println("線程" + Thread.currentThread().getName() + ",寫入數據成功.....");
		
		try {
			cyclicBarrier.await();
		} catch (Exception e) {
		}
		System.out.println("所有線程執行完畢..........");
	}

}

public class Test001 {

	public static void main(String[] args) {
		CyclicBarrier cyclicBarrier=new CyclicBarrier(5);
		for (int i = 0; i < 5; i++) {
			Writer writer = new Writer(cyclicBarrier);
			writer.start();
		}
	}

}

(計數信號量)Semaphore

      Semaphore是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做自己的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。Semaphore可以用來構建一些對象池,資源池之類的,比如數據庫連接池,我們也可以創建計數爲1的Semaphore,將其作爲一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。它的用法如下:

      availablePermits函數用來獲取當前可用的資源數量

wc.acquire(); //申請資源
wc.release();// 釋放資源
	// 創建一個計數閾值爲5的信號量對象  
    	// 只能5個線程同時訪問  
    	Semaphore semp = new Semaphore(5);  
    	  
    	try {  
    	    // 申請許可  
    	    semp.acquire();  
    	    try {  
    	        // 業務邏輯  
    	    } catch (Exception e) {  
    	  
    	    } finally {  
    	        // 釋放許可  
    	        semp.release();  
    	    }  
    	} catch (InterruptedException e) {  
    	  
    	}  

案例:

      需求: 一個廁所只有3個坑位,但是有10個人來上廁所,那怎麼辦?假設10的人的編號分別爲1-10,並且1號先到廁所,10號最後到廁所。那麼1-3號來的時候必然有可用坑位,順利如廁,4號來的時候需要看看前面3人是否有人出來了,如果有人出來,進去,否則等待。同樣的道理,4-10號也需要等待正在上廁所的人出來後才能進去,並且誰先進去這得看等待的人是否有素質,是否能遵守先來先上的規則。

      代碼:

class ThradDemo001 extends Thread {
	private String name;
	private Semaphore wc;

	public ThradDemo001(String name, Semaphore wc) {
		this.name = name;
		this.wc = wc;
	}

	@Override
	public void run() {
		// 剩下的資源
		int availablePermits = wc.availablePermits();
		if (availablePermits > 0) {
			System.out.println(name + "天助我也,終於有茅坑了.....");
		} else {
			System.out.println(name + "怎麼沒有茅坑了...");
		}
		try {
			// 申請資源
			wc.acquire();
		} catch (InterruptedException e) {

		}
		System.out.println(name + "終於上廁所啦.爽啊" + ",剩下廁所:" + wc.availablePermits());
		try {
			Thread.sleep(new Random().nextInt(1000));
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println(name + "廁所上完啦!");
		// 釋放資源
		wc.release();
	}
}

public class TestSemaphore {

	public static void main(String[] args) {
		Semaphore semaphore = new Semaphore(3);
		for (int i = 1; i <= 10; i++) {
			ThradDemo001 thradDemo001 = new ThradDemo001("第" + i + "個人", semaphore);
			thradDemo001.start();
		}
	}

} 

併發隊列

      在併發隊列上JDK提供了兩套實現,一個是以ConcurrentLinkedQueue爲代表的高性能隊
列非阻塞5️⃣,一個是以BlockingQueue接口爲代表的阻塞隊列,無論哪種都繼承自Queue。

阻塞隊列與非阻塞隊

      阻塞隊列與普通隊列的區別在於,當隊列是空的時,從隊列中獲取元素的操作將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操作會被阻塞。試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其他的線程往空的隊列插入新的元素。同樣,試圖往已滿的阻塞隊列中添加新元素的線程同樣也會被阻塞,直到其他的線程使隊列重新變得空閒起來,如從隊列中移除一個或者多個元素,或者完全清空隊列.

1.ArrayDeque, (數組雙端隊列)
2.PriorityQueue, (優先級隊列)
3.ConcurrentLinkedQueue, (基於鏈表的併發隊列)
4.DelayQueue, (延期阻塞隊列)(阻塞隊列實現了BlockingQueue接口)
5.ArrayBlockingQueue, (基於數組的併發阻塞隊列)
6.LinkedBlockingQueue, (基於鏈表的FIFO阻塞隊列)
7.LinkedBlockingDeque, (基於鏈表的FIFO雙端阻塞隊列)
8.PriorityBlockingQueue, (帶優先級的無界阻塞隊列)
9.SynchronousQueue (併發同步阻塞隊列)

ConcurrentLinkedDeque

      ConcurrentLinkedQueue : 是一個適用於高併發場景下的隊列,通過無鎖的方式,實現了高併發狀態下的高性能,通常ConcurrentLinkedQueue性能好於BlockingQueue.它
是一個基於鏈接節點的無界線程安全隊列。該隊列的元素遵循先進先出的原則。頭是最先
加入的,尾是最近加入的,該隊列不允許null元素。

      ConcurrentLinkedQueue重要方法:

      add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中這倆個方法沒有任何區別)

      poll() 和peek() 都是取頭元素節點,區別在於前者會刪除元素,後者不會。

ConcurrentLinkedDeque q = new ConcurrentLinkedDeque();
q.offer("餘勝軍");
q.offer("碼雲");
q.offer("螞蟻課堂");
q.offer("張傑");
q.offer("艾姐");
//從頭獲取元素,刪除該元素
System.out.println(q.poll());
//從頭獲取元素,不刪除該元素
System.out.println(q.peek());
//獲取總長度
System.out.println(q.size());

BlockingQueue

      阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:
      在隊列爲空時,獲取元素的線程會等待隊列變爲非空。
      當隊列滿時,存儲元素的線程會等待隊列可用。
      阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。
      BlockingQueue即阻塞隊列,從阻塞這個詞可以看出,在某些情況下對阻塞隊列的訪問可能會造成阻塞。被阻塞的情況主要有如下兩種:

  1. 當隊列滿了的時候進行入隊列操作
  2. 當隊列空了的時候進行出隊列操作

      因此,當一個線程試圖對一個已經滿了的隊列進行入隊列操作時,它將會被阻塞,除非有另一個線程做了出隊列操作;同樣,當一個線程試圖對一個空隊列進行出隊列操作時,它將會被阻塞,除非有另一個線程進行了入隊列操作。

      在Java中,BlockingQueue的接口位於java.util.concurrent 包中(在Java5版本開始提供),由上面介紹的阻塞隊列的特性可知,阻塞隊列是線程安全的。

      在新增的Concurrent包中,BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數據的問題。通過這些高效並且線程安全的隊列類,爲我們快速搭建高質量的多線程程序帶來極大的便利。本文詳細介紹了BlockingQueue家庭中的所有成員,包括他們各自的功能以及常見使用場景。

認識BlockingQueue

      阻塞隊列,顧名思義,首先它是一個隊列,而一個隊列在數據結構中所起的作用大致如下圖所示:

      從上圖我們可以很清楚看到,通過一個共享的隊列,可以使得數據由隊列的一端輸入,從另外一端輸出;

      常用的隊列主要有以下兩種:(當然通過不同的實現方式,還可以延伸出很多不同類型的隊列,DelayQueue就是其中的一種)
      先進先出(FIFO):先插入的隊列的元素也最先出隊列,類似於排隊的功能。從某種程度上來說這種隊列也體現了一種公平性。
      後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件。
       多線程環境中,通過隊列可以很容易實現數據共享,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的數據共享。假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把準備好的數據共享給消費者線程,利用隊列的方式來傳遞數據,就可以很方便地解決他們之間的數據共享問題。但如果生產者和消費者在某個時間段內,萬一發生數據處理速度不匹配的情況呢?理想情況下,如果生產者產出數據的速度大於消費者消費的速度,並且當生產出來的數據累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的數據處理完畢,反之亦然。然而,在concurrent包發佈以前,在多線程環境下,我們每個程序員都必須去自己控制這些細節,尤其還要兼顧效率和線程安全,而這會給我們的程序帶來不小的複雜度。好在此時,強大的concurrent包橫空出世了,而他也給我們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)

ArrayBlockingQueue

      ArrayBlockingQueue是一個有邊界的阻塞隊列,它的內部實現是一個數組。有邊界的意思是它的容量是有限的,我們必須在其初始化的時候指定它的容量大小,容量大小一旦指定就不可改變。
      ArrayBlockingQueue是以先進先出的方式存儲數據,最新插入的對象是尾部,最新移出的對象是頭部。下面是一個初始化和使用ArrayBlockingQueue的例子:

<String> arrays = new ArrayBlockingQueue<String>(3);
	arrays.add("李四");
	 arrays.add("張軍");
	arrays.add("張軍");
	// 添加阻塞隊列
	arrays.offer("張三", 1, TimeUnit.SECONDS);

LinkedBlockingQueue

      LinkedBlockingQueue阻塞隊列大小的配置是可選的,如果我們初始化時指定一個大小,它就是有邊界的,如果不指定,它就是無邊界的。說是無邊界,其實是採用了默認大小爲Integer.MAX_VALUE的容量 。它的內部實現是一個鏈表。

      和ArrayBlockingQueue一樣,LinkedBlockingQueue 也是以先進先出的方式存儲數據,最新插入的對象是尾部,最新移出的對象是頭部。下面是一個初始化和使LinkedBlockingQueue的例子:

LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.add("張三");
linkedBlockingQueue.add("李四");
linkedBlockingQueue.add("李四");
System.out.println(linkedBlockingQueue.size());

PriorityBlockingQueue

      PriorityBlockingQueue是一個沒有邊界的隊列,它的排序規則和 java.util.PriorityQueue一樣。需要注 意,PriorityBlockingQueue中允許插入null對象。所有插入PriorityBlockingQueue的對象必須實現 java.lang.Comparable接口,隊列優先級的排序規則就
是按照我們對這個接口的實現來定義的。另外,我們可以從PriorityBlockingQueue獲得一個迭代器Iterator,但這個迭代器並不保證按照優先級順 序進行迭代。

      下面我們舉個例子來說明一下,首先我們定義一個對象類型,這個對象需要實現Comparable接口:

SynchronousQueue

      SynchronousQueue隊列內部僅允許容納一個元素。當一個線程插入一個元素後會被阻塞,除非這個元素被另一個線程消費。

      使用BlockingQueue模擬生產者與消費者

class ProducerThread implements Runnable {
	private BlockingQueue<String> blockingQueue;
	private AtomicInteger count = new AtomicInteger();
	private volatile boolean FLAG = true;

	public ProducerThread(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "生產者開始啓動....");
		while (FLAG) {
			String data = count.incrementAndGet() + "";
			try {
				boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
				if (offer) {
					System.out.println(Thread.currentThread().getName() + ",生產隊列" + data + "成功..");
				} else {
					System.out.println(Thread.currentThread().getName() + ",生產隊列" + data + "失敗..");
				}
				Thread.sleep(1000);
			} catch (Exception e) {

			}
		}
		System.out.println(Thread.currentThread().getName() + ",生產者線程停止...");
	}

	public void stop() {
		this.FLAG = false;
	}

}

class ConsumerThread implements Runnable {
	private volatile boolean FLAG = true;
	private BlockingQueue<String> blockingQueue;

	public ConsumerThread(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "消費者開始啓動....");
		while (FLAG) {
			try {
				String data = blockingQueue.poll(2, TimeUnit.SECONDS);
				if (data == null || data == "") {
					FLAG = false;
					System.out.println("消費者超過2秒時間未獲取到消息.");
					return;
				}
				System.out.println("消費者獲取到隊列信息成功,data:" + data);

			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}

}

public class Test0008 {

	public static void main(String[] args) {
		BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
		ProducerThread producerThread = new ProducerThread(blockingQueue);
		ConsumerThread consumerThread = new ConsumerThread(blockingQueue);
		Thread t1 = new Thread(producerThread);
		Thread t2 = new Thread(consumerThread);
		t1.start();
		t2.start();
		//10秒後 停止線程..
		try {
			Thread.sleep(10*1000);
			producerThread.stop();
		} catch (Exception e) {
			// TODO: handle exception
		}
	}

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