Java併發編程基礎構建模塊(05)——同步工具類

        前面介紹了同步容器類,阻塞隊列等,這裏說說同步工具類,同步工具類主要是根據自身的狀態來協調線程的控制流,阻塞隊列就可以作爲同步工具類,其他的同步工具類還包括:信號量(Semaphore)、閉鎖(Latch)、柵欄(Barrier)等,也可以自己創建工具。

        同步工具類大體原理有3點:

        1、 用一些屬性來表示某個狀態,這些狀態決定執行同步工具類的線程是等待,還是繼續執行;

        2、 提供了一些操作這些狀態的方法;

        3、 還提供另一些方法,用於高效地等待同步工具類進入到預期狀態(線程是無法控制具體怎麼執行的,只能修改狀態,線程執行到可以改變的時候根據狀態做出改變)。

        下面說說剛纔提到了信號量、柵欄、閉鎖等:

        信號量(Semaphore):信號量維護了一個固定容量的許可集,每一個許可被獲取後則不可用,直到這個許可被釋放,當獲取許可時發現沒有可用的許可,會阻塞當前線程,直到有可用的許可爲止(或者被中斷)。Semaphore構造時需要指定數量和是否公平(公平情況下誰先阻塞,就會優先給誰,但是性能沒非公平的好),acquire方法獲取許可,release方法釋放許可,可以一次獲取多個,也可以釋放多個。

        信號量主要作用其實就是對資源訪問的一個限制,比如限制同時訪問某個資源的操作數量,或者限制同時執行某個操作的數量,常用於實現某種資源池,或者對容器加邊界。如下代碼實現一個池:

        

        再來說說閉鎖,閉鎖(Latch):一種同步工具類,可以延遲線程進度(其實就是阻塞線程)直到到終止狀態。通俗理解,閉鎖相當於一扇門:閉鎖到達結束狀態前,門一直關閉,任何線程都無法通過(阻塞),當到達結束狀態時,門開,所有線程馬上通過(繼續執行),閉鎖結束,門以後一直處於敞開狀態(就是一次性的門)。

        比如線程S,它需要等線程A,B,C都執行完畢後,再繼續執行,此時閉鎖就可以方便的實現這個功能。閉鎖的主要使用場景:

        1、 開/關鎖存器,如確保某個操作所需要資源都被初始化之後才繼續執行;確保某個服務在其所依賴的其他服務都已經啓動後才啓動;等待某個操作的所有參與者全部就緒再繼續執行等。

        2、在一些應用場合中,爲了速度或其他等原因,需要將一個問題分解成N個部分去解決,或將一個大任務分解成多個小任務同時並行去執行,也就是由原來單線程執行一個大任務分解成一堆多線程並行執行的小任務,全部執行完畢後(整個問題都解決了),再繼續向下執行。

        CountDownLatch就是JDK中一個非常好的閉鎖實現,CountDownLatch有一個整數計數器,初始化時必須設置一個值(大於0),await方法會使當前線程阻塞,等待直到計數器爲0再繼續向下執行,countDown方法能減少計數器。如上面提到的線程S,它需要等線程A,B,C都執行完畢後再繼續執行,可以創建一個計數器是3的CountDownLatch,S使用await方法等待,線程A,B,C每個執行完畢後可以countDown一下,ABC都執行完畢後,計數器爲0,S繼續向下執行。

        舉個實際開發中例子,我們給法院做軟件,法院周邊系統需要從法院的審判系統(法院的主系統)中抽取案件信息(說白了就是信息抽取操作),需要抽取民事、刑事、執行3類案件,單線程抽取一次時間很長(比如每類案件抽取1小時,就要花3小時),由於各類案件相互獨立,就可以多線程一起抽取,3個線程一起抽取就快了很多(可能1個小時多點就結束了),但是主線程必須等待3類案件全部抽取結束,才繼續執行後續的案件的整合、計算等操作。示例代碼如下:

        

        其實,還有FutureTask也可以用於閉鎖,FutureTask其實就是Future和Callable的結合體,相當於有3個狀態:等待運行,運行中,運行完成。因爲實現了Future,所以調用get方法可以阻塞當前線程,直到運行FutureTask的線程計算出結果爲止(還有個2個方法停止,FutureTask拋出異常,或者當前線程被中斷),如果已經計算出結果了,get方法會馬上返回這個結果。這樣就可以實現類似所需要的資源都被初始化之後才繼續執行;確保某個服務在其所依賴的其他服務都已經啓動後才啓動等情況。比較簡單的實現方式就是啓動所有依賴操作的線程後,逐個去get,沒執行完則阻塞,執行完的給結果,最終所有結果的獲得了,也不耽誤時間。

        其實FutureTask多用於一些時間比較長的計算,主線程可以在完成自己的任務後,再去獲取結果。相比CountDownLatch,兩者使用場景不同,沒法比較,但是針對閉鎖功能來說,CountDownLatch更像一個鎖,而FutureTask只能說“能實現閉鎖功能”,但是某些場景,如需要返回值時,FutureTask能更方便的獲取結果(直接get就行),而CountDownLatch只能將每個線程的計算結果放到共享區域(線程間資源共享)。

        最後說說柵欄,柵欄(Barrier):它允許一組線程互相等待,直到到達某個公共屏障點(common barrier point)。類似於閉鎖,它能夠阻塞一組線程直到某個事件的發生,與閉鎖不同之處在於閉鎖用於等待事件,而柵欄用於等待其他線程。一個是根據事件的,一個是根據線程的。

        CyclicBarrier是柵欄的一種實現,它在釋放等待線程後可以重用,也就是說它是循環的柵欄。其實這裏可以看出來,CyclicBarrier與CountDownLatch比較相似,都能實現線程之間的相互等待,由於CountDownLatch的countDown()不會引起阻塞,所以CountDownLatch更多應用於主線程等待所有子線程結束後再繼續執行的情況,是基於事件的,一次性的;而CyclicBarrier計數達到指定後會重新循環使用,所以CyclicBarrier可以用在所有子線程之間互相等待多次的情形,是基於線程的,可以循環的。

        CyclicBarrier常用於並行迭代算法,這種算法通常將一個問題拆分成一系列的相互獨立的子問題,而且需要反覆執行,反覆再某個點處理結果(如果執行1次,CountDownLatch比較合適)。舉個例子,處理大位圖圖片,需要逐個像素點的讀取並轉換,整個圖片所有像素點讀取並轉換完畢後,需要整體處理,此時單線程處理比較慢,使用多線程,每個線程處理圖片的一部分(讀取和轉換),整個圖像像素點都讀取和轉換完畢後,再整體處理,處理完一張圖片後,馬上處理下一張。代碼如下:

        圖片處理主類:

        

        處理每部分的線程:

        

        其實Exchanger也是一種形式的柵欄,他是一種雙向的柵欄,可以看成SynchronousQueue的雙向形式,主要用於雙方在柵欄位置交換數據,當雙方的執行時間不對稱時,Exchanger會阻塞先到的一方,等待另一方完成。當一個線程調用exchange方法時,如果有其他線程之前調用過exchange方法,則會進行交換,如果沒有其他線程調用過,則阻塞當前線程,直到有其他線程調用exchange進行交換爲止,這塊就不詳說了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章