目錄
ReentrantLock、synchronized 大家經常使用也基本瞭解的比較多,今天介紹一下其他幾個很棒的併發工具類
一、CountDownLatch 和 CyclicBarrier
例子背景:4個工作,開發工作1、開發工作2、測試、上線。
單線程執行所有的工作,整個執行順序爲串行,佔用的資源少但是完成時長就會更長。
串行開發
private Boolean WORK = Boolean.TRUE;
@Test
public void one() throws InterruptedException {
Integer i = 0;
//一直有工作
while (WORK){
long start = System.currentTimeMillis();
System.out.println("######開始第" + (++i) +"次需求######");
System.out.println("【開發工作1】開始");
Thread.sleep(1000);
System.out.println("【開發工作1】完成");
System.out.println("【開發工作2】開始");
Thread.sleep(3000);
System.out.println("【開發工作2】完成");
System.out.println("【測試】完成");
System.out.println("【上線】完成");
long use = System.currentTimeMillis() - start;
System.out.println("######完成第" + i +"次需求用時:" + use + "######");
}
}
執行結果:
通過結果可以看到單線程執行基本耗時在:4008ms
🤔:產品上線的效率還能提高不?
👨💻:可以,我們多加幾個人。請看CountDownLatch
1.1 CountDownLatch
爲了提高處理效率我們可以開啓多線程,但是需要注意一個問題那麼就是開發工作1和開發工作2 要全部完成才能開始測試,所以需要讓子線程與主線程的一個協同同步,即主線程等待多個子線程。
針對此類問題可以通過Java 的CountDownLatch 來完成線程間的協調。
多人協作
1.1.1 CountDownLatch使用
CountDownLatch類位於java.util.concurrent包下,利用它可以實現類似計數器的功能。比如有一個任務A,它要等待其他4個任務執行完畢之後才能執行,此時就可以利用CountDownLatch來實現這種功能了。
CountDownLatch類只提供了一個構造器:
/**
* Constructs a {@code CountDownLatch} initialized with the given count.
*
* @param count the number of times {@link #countDown} must be invoked
* before threads can pass through {@link #await}
* @throws IllegalArgumentException if {@code count} is negative
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
重要的三個操作方法:
//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
////和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//將count值減1
public void countDown() {
sync.releaseShared(1);
}
瞭解了基本的方法,實現一下具體的Demo:
/**
* 通過兩個工作並行完成,但是需要全部完成後纔可以提測,因此主線程需要等待兩個線程才能開始繼續
* 本例使用 CountDownLatch 來完成子線程與主線程的協同
* @throws InterruptedException
*/
@Test
public void twoCountDownLatch() throws InterruptedException {
final Integer[] i = {0};
//一直有工作
while (WORK){
long start = System.currentTimeMillis();
System.out.println("######開始第" + (++i[0]) +"次需求######");
CountDownLatch countDownLatch = new CountDownLatch(2);
//第一組開發工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (i[0]) + "次【開發工作1】開始");
Thread.sleep(1000);
System.out.println("第" + (i[0]) + "次【開發工作1】完成");
countDownLatch.countDown();
}
}).start();
//第二組開發工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (i[0]) + "次【開發工作2】開始");
Thread.sleep(3000);
System.out.println("第" + (i[0]) + "次【開發工作2】完成");
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("第" + (i[0]) + "次【測試】完成");
System.out.println("第" + (i[0]) + "次【上線】完成");
long use = System.currentTimeMillis() - start;
System.out.println("######完成第" + (i[0]) +"次需求用時:" + use + "ms ######");
}
}
執行結果:
通過結果可以看到,讓開發工作並行後明顯縮短了每次產品的交付時間。由4008ms 減少至 3003ms
1.1.2 關鍵源碼
CountDownLatch 內部定義了一個 Syns 類,Sync繼承了大名鼎鼎的AQS(https://km.sankuai.com/page/155759840)。
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
🤔:還有沒有辦法再提高一些效率呢,人多了但是有一些時候空閒有點浪費?
👨💻:當然可以!美團的開發是宇宙最強😎💪,我們可以一個工作接着一個工作的幹根本停不下來。
那麼怎麼提高RD的工作效率呢!請看CyclicBarrier
1.2 CyclicBarrier
通過上一工作流程我們可以看到在測試和上線階段RD1和RD2 處於空閒狀態,我們可以讓上一個項目的測試和下一個產品開發進行並行。
提出一個假設前提:RD1 和 RD 2 要一起開始新工作,不能獨自去開發新產品。
1.因此我們需要讓RD1 和 RD 2 協同完成工作,不能RD1 完成一個工作就去開始下一個,需要等待RD2
2.需要在開發工作完成後能夠通知測試工作開始
所以又要RD 互相通信等待不能獨自去開發下一個產品,又要RD 能夠通知QA 進行測試工作。因此可以使用 CyclicBarrier 來實現線程間的互相等待
多人協作2.0
1.2.1 CyclicBarrier使用
CyclicBarrier字面意思迴環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行。叫做迴環是因爲當所有等待線程都被釋放以後,CyclicBarrier可以被重用。我們暫且把這個狀態就叫做barrier,當調用await()方法之後,線程就處於barrier了
CyclicBarrier 構造函數有兩個。一個支持一次迴環結束後進行動作的執行;另一個只是幫助完成多線程間的協同控制。參數parties指讓多少個線程或者任務等待至barrier狀態;參數barrierAction爲當這些線程都達到barrier狀態時會執行的內容。
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and which
* will execute the given barrier action when the barrier is tripped,
* performed by the last thread entering the barrier.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @param barrierAction the command to execute when the barrier is
* tripped, or {@code null} if there is no action
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and
* does not perform a predefined action when the barrier is tripped.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties) {
this(parties, null);
}
重要的三個操作方法:
類似於CountDownLatch,CyclicBarrier 提供了兩個await()方法主要區別在於一個設置了超時時間,可以防止線程“一直等待”
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
瞭解了基本的方法,實現一下具體的Demo:
Executor executor = Executors.newFixedThreadPool(1);
@Test
public void threeCyclicBarrier() throws InterruptedException {
//計數器
final Integer[] x = {0};
final Integer[] y = {0};
final Integer[] z = {0};
final Integer[] j = {0};
final Long[] start = new Long[10000];
CyclicBarrier barrier = new CyclicBarrier(2,
() -> {
executor.execute(() ->
{
System.out.println("第" + (++j[0]) + "次【測試】完成");
System.out.println("第" + (j[0]) + "次【上線】完成");
long use = System.currentTimeMillis() - start[j[0]];
System.out.println("######完成第" + j[0] + "次需求用時:" + use + "ms ######");
});
});
//一直有工作
while (WORK) {
System.out.println("######開始第" + (++x[0]) + "次需求######");
start[x[0]] = System.currentTimeMillis();
//第一組開發工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (++y[0]) + "次【開發工作1】開始");
Thread.sleep(1000);
System.out.println("第" + (y[0]) + "次【開發工作1】完成");
barrier.await();
}
}).start();
//第二組開發工作
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("第" + (++z[0]) + "次【開發工作2】開始");
Thread.sleep(3000);
System.out.println("第" + (z[0]) + "次【開發工作2】完成");
barrier.await();
}
}).start();
}
}
👨💻:效率實在太高了,一次完整產品才花費 1008ms
本質上還是提供併發的工作
1.2.2 關鍵源碼
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
通過CountDownLatch 也可以完成上述demo 的協同效果,但是需要我們管理每組工作的CountDownLatch ,進行多線程的數據交互管理,而CyclicBarrier則可以方便的“自動綁定”互相等待線程結束時的動作。 大家可以自己動手實現以下CountDownLatch 版的demo 完成宇宙最強開發的無縫銜接工作效率
舉一個其他的例子緩解一下審美疲勞,思路可以用來實現上邊的Demo
實現N個線程同時啓動,然後依次打印各自的序號 😁
@Test
public void testThread(){
int n = 10;
CountDownLatch countDownLatchCur = new CountDownLatch(1);
Te t = new Te(0, countDownLatchCur);
Te tMain = t;
tMain.start();
for (int i = 1; i < n ; i++){
t = new Te(i, t.getCountDownLatchMy()).start();
}
//測試一下打印最後一個。 but 第一個不開始,你真的打印不出來哦!
t.start();
//讓第一個線程開始打印吧,大家都等半天了
tMain.getCountDownLatch().countDown();
}
public class Te{
Integer cur ;
CountDownLatch countDownLatch;
CountDownLatch countDownLatchMy = new CountDownLatch(1);
public Te(Integer cur, CountDownLatch countDownLatch){
this.cur = cur;
this.countDownLatch = countDownLatch;
}
public Te start (){
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cur);
countDownLatchMy.countDown();
}).start();
return this;
}
public CountDownLatch getCountDownLatchMy(){
return this.countDownLatchMy;
}
public CountDownLatch getCountDownLatch(){
return this.countDownLatch;
}
}
1.3 小結:
CountDownLatch 和 CyclicBarrier 是 Java 併發包提供的兩個非常易用的線程同步工具類,這兩個工具類用法的區別在這裏還是有必要再強調一下:CountDownLatch 主要用來解決一個線程等待多個線程的場景,可以類比一個產品上線,只有各個依賴服務都部署成功才能開啓新產品的操作入口;而 CyclicBarrier 是一組線程之間互相等待,類似家庭聚會人齊了纔會開飯。除此之外 CountDownLatch 的計數器是不能循環利用的,也就是說一旦計數器減到 0,再有線程調用 await(),該線程會直接通過。但 CyclicBarrier 的計數器是可以循環利用的,而且具備自動重置的功能,一旦計數器減到 0 會自動重置到你設置的初始值。除此之外,CyclicBarrier 還可以設置回調函數,可以說是功能豐富。
二、CompletionService
2.1 CompletionService使用
CompletionService 接口的實現類是 ExecutorCompletionService,這個實現類的構造方法有兩個,分別是:ExecutorCompletionService(Executor executor);ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)。這兩個構造方法都需要傳入一個線程池,如果不指定 completionQueue,那麼默認會使用無界的 LinkedBlockingQueue。任務執行結果的 Future 對象就是加入到 completionQueue 中。
/**
* Creates an ExecutorCompletionService using the supplied
* executor for base task execution and a
* {@link LinkedBlockingQueue} as a completion queue.
*
* @param executor the executor to use
* @throws NullPointerException if executor is {@code null}
*/
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
/**
* Creates an ExecutorCompletionService using the supplied
* executor for base task execution and the supplied queue as its
* completion queue.
*
* @param executor the executor to use
* @param completionQueue the queue to use as the completion queue
* normally one dedicated for use by this service. This
* queue is treated as unbounded -- failed attempted
* {@code Queue.add} operations for completed tasks cause
* them not to be retrievable.
* @throws NullPointerException if executor or completionQueue are {@code null}
*/
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue) {
if (executor == null || completionQueue == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
}
CompletionService 的實現原理是內部維護了一個阻塞隊列,當任務執行結束就把任務的執行結果加入到阻塞隊列中,把任務執行結果的 Future 對象加入到阻塞隊列中。
private final BlockingQueue<Future<V>> completionQueue;
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture<V>(f, completionQueue));
return f;
}
Demo中,我們沒有指定 completionQueue,因此默認使用無界的 LinkedBlockingQueue。
通過 CompletionService 接口提供的 submit() 方法提交了三個查詢地理位置信息操作,這三個操作將會被 CompletionService 異步執行。最後,我們通過 CompletionService 接口提供的 take() 方法獲取一個 Future 對象,調用 Future 對象的 get() 方法就能返回查詢地理位置信息操作的執行結果了。
CompletionService 的 3 個方法take()、poll() 都是和阻塞隊列相關的,都是從阻塞隊列中獲取並移除一個元素;它們的區別在於如果阻塞隊列是空的,那麼調用 take() 方法的線程會被阻塞,而 poll() 方法會返回 null 值。 poll(long timeout, TimeUnit unit) 方法支持以超時的方式獲取並移除阻塞隊列頭部的一個元素,如果等待了 timeout unit 時間,阻塞隊列還是空的,那麼該方法會返回 null 值。
/**
*僞代碼,無法執行
*/
//同時查詢多個地圖服務,然後拼裝一個準確的地理位置信息描述
private Executor executor;
@Test
public void testC(){
CompletionService<Map<String, String>> cs = new ExecutorCompletionService<>(executor);
Map<String, String> selectMap = new ConcurrentHashMap<>();
selectMap.put("百度地圖", "地點");
selectMap.put("騰訊地圖", "地點");
selectMap.put("高德地圖", "地點");
for (Map.Entry<String, String> entry : selectMap.entrySet()) {
cs.submit(() -> seletcMapService.select(entry.getKey(), entry.getValue());
}
Map<String, MapInfo> MapInfoMap = new ConcurrentHashMap<>();
for (int i = 0; i < selectMap.size(); i++) {
try {
// CompletionService 內部的 BlockingQueue<Future<V>> completionQueue;
MapInfo mapInfo = cs.take().get();
MapInfoMap.put(mapInfo.getkey(), mapInfo);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
log.error("查詢數據異常",e);
}
}
}
2.2 小結
除了CompletionService,我們更經常使用Stream,那麼到底應該選哪個呢,引用《Java 8實戰》一段知識。
摘自:Java8 實戰-Page-234
參考:
《Java 併發編程》
https://time.geekbang.org/column/article/89461