04.05 在使用線程時,要優先選擇線程池

在Java1.5之前,實現多線程比較麻煩,需要自己啟動線程,並關注同步資源,防止出現線程死鎖等問題,在1.5版本之後引入了並行計算框架,大大簡化了多線程開發。我們知道一個線程有五個狀態:新建狀態(NEW)、可運行狀態(Runnable,也叫作運行狀態)、阻塞狀態(Blocked)、等待狀態(Waiting)、結束狀態(Terminated),線程的狀態只能由新建轉變為了運行狀態後才能被阻塞或等待,最後終結,不可能產生本末倒置的情況,比如把一個結束狀態的線程轉變為新建狀態,則會出現異常,例如如下代碼會拋出異常:

在使用線程時,要優先選擇線程池

此段程序運行時會報java.lang.IllegalThreadStateException異常,原因就是不能從結束狀態直接轉變為運行狀態,我們知道一個線程的運行時間分為3部分:T1為線程啟動時間,T2為線程的運行時間,T3為線程銷燬時間,如果一個線程不能被重複使用,每次創建一個線程都需要經過啟動、運行、銷燬時間,這勢必增大系統的響應時間,有沒有更好的辦法降低線程的運行時間呢?

T2是無法避免的,只有通過優化代碼來實現降低運行時間。T1和T2都可以通過線程池(Thread Pool)來縮減時間,比如在容器(或系統)啟動時,創建足夠多的線程,當容器(或系統)需要時直接從線程池中獲得線程,運算出結果,再把線程返回到線程池中___ExecutorService就是實現了線程池的執行器,我們來看一個示例代碼:

在使用線程時,要優先選擇線程池

此段代碼首先創建了一個包含兩個線程的線程池,然後在線程池中多次運行線程體,輸出運行時的線程名稱,結果如下:

pool-1-thread-1

pool-1-thread-2

pool-1-thread-1

pool-1-thread-2

  本次代碼執行了4遍線程體,按照我們之前闡述的" 一個線程不可能從結束狀態轉變為可運行狀態 ",那為什麼此處的2個線程可以反覆使用呢?這就是我們要搞清楚的重點。

線程池涉及以下幾個名詞:

  • 工作線程(Worker):線程池中的線程,只有兩個狀態:可運行狀態和等待狀態,沒有任務時它們處於等待狀態,運行時它們循環的執行任務。

  • 任務接口(Task):這是每個任務必須實現的接口,以供工作線程調度器調度,它主要規定了任務的入口、任務執行完的場景處理,任務的執行狀態等。這裡有兩種類型的任務:具有返回值(異常)的Callable接口任務和無返回值併兼容舊版本的Runnable接口任務。

  • 任務對列(Work Quene):也叫作工作隊列,用於存放等待處理的任務,一般是BlockingQuene的實現類,用來實現任務的排隊處理。

我們首先從線程池的創建說起,Executors.newFixedThreadPool(2)表示創建一個具有兩個線程的線程池,源代碼如下:

在使用線程時,要優先選擇線程池

這裡使用了LinkedBlockingQueue作為隊列任務管理器,所有等待處理的任務都會放在該對列中,需要注意的是,此隊列是一個阻塞式的單端隊列。線程池建立好了,那就需要線程在其中運行了,線程池中的線程是在submit第一次提交任務時建立的,代碼如下:

在使用線程時,要優先選擇線程池

此處的代碼關鍵是execute方法,它實現了三個職責。

  • 創建足夠多的工作線程數,數量不超過最大線程數量,並保持線程處於運行或等待狀態。

  • 把等待處理的任務放到任務隊列中

  • 從任務隊列中取出任務來執行

其中此處的關鍵是工作線程的創建,它也是通過new Thread方式創建的一個線程,只是它創建的並不是我們的任務線程(雖然我們的任務實現了Runnable接口,但它只是起了一個標誌性的作用),而是經過包裝的Worker線程,代碼如下:

在使用線程時,要優先選擇線程池

此處為示意代碼,刪除了大量的判斷條件和鎖資源。execute方法是通過Worker類啟動的一個工作線程,執行的是我們的第一個任務,然後改線程通過getTask方法從任務隊列中獲取任務,之後再繼續執行,但問題是任務隊列是一個BlockingQuene,是阻塞式的,也就是說如果該隊列的元素為0,則保持等待狀態,直到有任務進入為止,我們來看LinkedBlockingQuene的take方法,代碼如下:

在使用線程時,要優先選擇線程池

分析到這裡,我們就明白了線程池的創建過程:創建一個阻塞隊列以容納任務,在第一次執行任務時創建做夠多的線程(不超過許可線程數),並處理任務,之後每個工作線程自行從任務對列中獲得任務,直到任務隊列中的任務數量為0為止,此時,線程將處於等待狀態,一旦有任務再加入到隊列中,即召喚醒工作線程進行處理,實現線程的可複用性。

使用線程池減少的是線程的創建和銷燬時間,這對於多線程應用來說非常有幫助,比如我們常用的Servlet容器,每次請求處理的都是一個線程,如果不採用線程池技術,每次請求都會重新創建一個新的線程,這會導致系統的性能符合加大,響應效率下降,降低了系統的友好性。

有討論,才有進步,大家各抒己見,讓每位同學學到不一樣的!


分享到:


相關文章: