Java 多線程中使用 JDK 自帶工具類實現計數器
摘要:使用 join() 方法的子線程對象正常執行 run() 中代碼,但當前線程會被無超時阻塞,等待執行 join() 方法的線程銷燬後,繼續執行被阻塞的當前線程。通過增加 join() 方法實現阻塞主線程,等待子線程完成後再進行彙總:。
前言
在實際開發過程中,經常遇到需要多線程並行的業務,最後需要進行將各個線程完成的任務進行彙總,但主線程一般會早於子線程結束,如果要想等各個子線程完成後再繼續運行主線程,這時就需要對各個線程是否執行完成進行標識,JDK 併發包中就給開發者提供了幾個不錯的使用工具類。
接下來將通過 Thread#join 方法以及 CountDownLatch、CyclicBarrier 類進行上面案例方案的分析。
Thread#join 方法
使用 join() 方法的子線程對象正常執行 run() 中代碼,但當前線程會被無超時阻塞,等待執行 join() 方法的線程銷燬後,繼續執行被阻塞的當前線程。join() 方法阻塞原理是該方法內使用 wait() 方法阻塞,源碼如下所示:
子線程 join() 完成時會調用 notifyAll() 來通知當前線程繼續執行接下來的代碼。
假如現在有兩個線程產生數據結果,最後將兩個線程結果進行相加,如果直接將兩個線程執行並進行彙總,如下實現代碼:
執行結果:
由於主線程的彙總計算可能早於子線程完成,所以這時獲取子線程結果爲空指針異常。
通過增加 join() 方法實現阻塞主線程,等待子線程完成後再進行彙總:
執行結果爲:
通過結果可以看到子線程彙總求和爲 3。此時主線程在兩個子線程銷燬前都處於等待狀態,直至兩個銷燬後主線程再執行彙總求和,所以兩個線程產生的值都已存在。
同時,子線程 join() 方法可以使當前線程無期限等待,也可以設置最長等待時長 join(long) 方法,無論子線程是否執行完成,當前線程會繼續執行後面代碼。使用方法加入超時參數即可,其它與 join() 方法使用相同。
CountDownLatch
CountDownLatch 可以使一個或多個線程等待其他線程完成操作後再繼續執行當前線程後面代碼。
CountDownLatch 的使用:首先創建 CountDownLatch 對象,通過傳入參數 int 構造 CountDownLatch 對象。該參數是值將要等待的執行點的數量。
CountDownLatch 中有幾個方法:
-
getCount()返回當前計數器數,即當前剩餘的等待數量。官方解釋說該方法通常用於調試和測試目的。
-
countDown每調用一次,計數器便會進行減 1 操作,但計數器必須大於 0。
-
await該方法會阻塞當前線程,直至計數器爲 0 時,就會不再阻塞當前線程。同時也提供 await(long timeout, TimeUnit unit) 方法,可設置超時時間。
利用 CountDownLatch 實現彙總求和案例,實現代碼如下:
執行結果如下:
上圖中求和結果爲 3,同時計數器爲 0。
通過查看 CountDownLatch 源碼,主要是通過一個繼承 AbstractQueuedSynchronizer 類的內部類 Sync 來實現的,可知其實現原理爲 AQS,這裏不進行展開講述。
CyclicBarrier
CyclicBarrier 是一個可循環使用的屏障。實現原理解釋,就是在一個或多個線程運行中設置一個屏障,線程到達這個屏障時會被阻塞,直到最後一個線程到達時,被屏障阻塞的線程繼續執行。
CyclicBarrier 構造方法有兩個, CyclicBarrier(intcount)
和 CyclicBarrier(intcount,RunnablebarrierAction)
:
-
單個
int
參數構造方法,表示構造到達屏障線程的數量。 -
一個
int
和一個Runnable
參數構造方法,前者參數表示到達屏障線程的數量,後者參數表示所有線程到達屏障後接下來要執行的代碼;
CyclicBarrier 中方法:
方法 | 說明 |
---|---|
await() | 阻塞前線程,等待 trip.signal() 或 trip.signalAll() 方法喚醒 |
await(long, TimeUnit) | 在 await() 上增加兩個參數,等待超時時間 timeout,單位爲 unit |
breakBarrier() | 放開屏障,設置標誌,喚醒被屏障阻塞的線程 |
isBroken() | 阻塞的線程是否被中斷 |
reset() | 重置 CyclicBarrier 對象 |
getNumberWaiting() | 當前被阻塞線程的數量 |
getParties() | 到達屏障的線程總數量,即創建時指定的數量 |
使用 CyclicBarrier 實現上面彙總:
執行結果:
執行完兩條子線程,並且在子線程裏調用 barrier.await()
後,屏障被打開,最後執行 CyclicBarrier 的最後的代碼邏輯。
通過上面 CyclicBarrier 的方法可知,CyclicBarrier 比 CountDownLatch 使用更加靈活,CyclicBarrier 的 reset() 方法可以重置計數器,而 CountDownLatch 則只能使用一次。同時,CyclicBarrier 擁有更多線程阻塞信息的方法提供使用,在使用過程中,提供更加靈活的使用方式。
總結
上面三種方式,均由 JDK 的併發包中提供的工具。在多線程協作任務中,對計數器場景問題的解決方案,實現 main 線程對 worker 線程的等待完成。在實際開發應用中,使用頻率也是非常之高。