Java併發編程實戰筆記_併發任務執行

        任務執行有2種處理機制,串行執行和並行執行。
        串行:同一時刻只能執行一個任務,程序簡單,安全性高,不涉及同步等情況,缺點也顯而易見,無法提高吞吐量和響應速度,適合任務數量很少並且執行時間很長時,或者只爲單個用戶使用,並且該用戶每次只發出一個請求。
        並行:同一時刻能執行多個任務,能提高CPU的利用率(尤其多核CPU情況下),提高吞吐量和響應速度等很多好處,但是也有缺點,程序設計複雜(各種同步,各種資源訪問,線程通信等),問題難以發現和排查,額外資源開銷(線程管理,線程池,CPU競爭等)等等。
        當然,不能絕對的說哪個好哪個壞,程序只有適不適合,沒有好壞。多線程併發執行更適合大部分場景,尤其在現在多核的時代的情況下,下面說說並行任務執行的設計。
        並行執行就要創建新線程,傳統的方式中我們顯式的創建線程:
        
        創建線程開銷:線程的創建和銷燬都是需要代價的,尤其處理大量非常簡單的請求情況下(比如每秒上百次請求,但是每個請求只處理0.1s就結束了,大部分web服務都是這樣的),創建和銷燬線程花費時間比執行任務時間還大,這樣“爲每個任務分配一個線程”方法就有點傻B了。
        這種“爲每個任務分配一個線程”的方法用在測試程序還可以,只要請求的增長速度不會超過服務器處理請求速度就行,但是生產環境這樣可能會帶來一些問題,我們說主要的3點:
        資源消耗:線程太多會佔用更多內存,也會競爭CPU降低性能,垃圾回收也是個麻煩事,只要線程數量能保證CPU始終忙碌(比如始終使用率90%左右)即可。
        穩定性:可創建線程的最大數量受很多方面制約,比如操作系統,JVM參數等,超了容易OutOfMemoryError,線程太多也容易導致某些操作執行失敗,莫名其妙的Error讓你崩潰。
        也就是說在一定範圍內,增加線程有助於提高吞吐量,但是再多就可能導致性能下降。其實這就是一個執行策略問題,並行任務要考慮很多東西,比如:
                1、在什麼線程中執行?
                2、任務按照什麼順序執行(FIFO、LIFO、優先級等)?
                3、同時併發執行任務最大有多少?
                4、等待隊列最多能等待多少個?
                5、如果系統由於過載而拒絕任務時,選擇哪個任務?如何通知被拒絕的任務?
                6、執行一個任務前後,應該進行哪些操作?
        其實,每當看到new Thread()這種形式,就應該想到使用線程池(僅僅只是想到,具體用不用要看實際環境),線程池的概念不說了,好壞優劣也不說了,至少上面提到的執行策略都可以進行配置。
        Java中創建線程池有很多辦法,比如使用ThreadPool,或者自己實現一個等等,其實JDK中是有現成的,更方便,就是Executors。Executors提供了10個方法創建線程池,其實就是5類方法,每類2個方法,一個提供自定義ThreadFactory來創建線程,一個不提供,下面說說這5類方法:
        
        說完創建就要繼續說回收,也就是線程池的生命週期。你會發現所有的newXXXThreadPool都是返回的ExecutorService,沒錯,線程池的生命週期就由ExecutorService控制,ExecutorService接口繼承了Executor接口,增加了控制生命週期的方法,線程池有3個狀態:運行(running),關閉(shutdown),終止(terminate):
        
        下面這段代碼,使用awaitTermination方法,每隔1s檢查一下線程池是否結束,沒結束繼續等待:
        
        順便說說ExecutorService擴展的其他方法,除了擴展生命週期方法外,還提供了新的調用執行任務的方法,原始的Executor只有execute方法來提交任務執行,ExecutorService新增了3類方法,每類中有2-3個方法,使用起來基本一致(就是有延遲沒延遲,有的用Runnable,有的Callable等):
        
        再簡單說說延遲任務和週期任務,JDK中有個Timer可以延遲、週期執行,Timer有個缺陷,就是執行任務時間長的時候會不精確,比如1分鐘執行1次,1次執行10分鐘,那麼第2次執行時,會連續調用10次(固定速率),或者從執行結束後重新計算時間(固定延遲),就這2種策略,比較麻煩。你可能會想,我啓用新線程執行,只是定時調用,不用等待執行任務的線程,尼瑪這不就是ScheduledThreadPool線程池做的嗎,所以涉及延遲、週期執行線程的問題,優先考慮ScheduledThreadPool。
        並行任務執行還涉及任務分配異構的問題,比如一組人負責洗碗,一組人負責烘乾,如何分配任務,讓雙方的人都忙碌,兩組人的比例如何分配,以後增加人時,如何保證相互不影響,這種情況沒考慮清楚,並行任務不但複雜,而且並不一定有效率的提升。再比如兩個任務分配給A、B兩個人,但是A執行時間是B的10倍,那麼並行比串行的速度僅僅提高了9%,而且在分解任務、分配任務時還需要一定的任務協調開銷,而且這種開銷不能超過節約出來的性能(比如上面的9%)。任務分配確實是一個問題,而且有非常多的情況,需要好好思考,下面介紹一個CompletionService類,是解決併發執行,順序獲取結果的一個接口,其實就是相當於Executor加上BlockingQueue,當子線程併發了一系列的任務以後,主線程需要實時地取回子線程任務的返回值並同時順序地處理這些返回值,誰先返回就先處理誰,這時使用CompletionService就非常方便了(其實可以自己用Executor加上BlockingQueue實現,也比較簡單,不過有JDK提供的方法,優雅方便)。
        在說一個實用的功能和場景,前面我們使用Future的get方法獲取結果,可以阻塞線程,一有結果馬上返回,非常方便,其實get方法還可以設置超時時間,如果超時則往下執行,使用場景是某個任務在指定時間內獲取不到結果,就不再需要它的結果了,放棄這個任務了。比如網站頁面的廣告,要從廣告商的服務器獲取廣告,2s獲取不到就不要了,這樣也不影響性能。Future的get(long timeout,TimeUnit unit)方法如果超時沒返回結果,會拋出TimeoutException異常,如果要取消執行,可以catch後執行Futrue的canel方法。比如:
        
        這個是單個的超時例子,如果是一組線程執行任務,超時後取消沒執行完的,可以考慮使用上面提到的invokeAll方法,非常好用。
        本次主要針對任務執行簡單討論,並行執行使程序複雜,Executor框架將任務提交和任務執行策略分離,解耦開來,同時支持多種不通類型的策略。要想將應用程序分解爲不同的任務時獲得最大好處,就必須定義清晰的任務邊界,某些程序任務邊界比較明顯,有些就要根據實際情況自行分析了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章