讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議125:優先選擇線程池 >

建議125:優先選擇線程池

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


public static void main(Stringargs)throws Exception{

//創建一個線程,新建狀態

Thread t=new Thread(new Runnable(){

public void run(){

System.out.println(\"線程在運行……\");

}

});

//運行狀態

t.start();

//是否是運行態,若不是則等待10毫秒

while(!t.getState().equals(Thread.State.TERMINATED)){

TimeUnit.MILLISECONDS.sleep(10);

}

//直接由結束態轉變為運行態

t.start();

}


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

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


public static void main(Stringargs){

//2個線程的線程池

ExecutorService es=Executors.newFixedThreadPool(2);

//多次執行線程體

for(int i=0;i<4;i++){

es.submit(new Runnable(){

public void run(){

System.out.println(Thread.currentThread().getName());

}

});

}

//關閉執行器

es.shutdown();

}


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


pool-1-thread-1

pool-1-thread-2

pool-1-thread-1

pool-1-thread-2


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

線程池的實現涉及以下三個名詞:

(1)工作線程(Worker)

線程池中的線程,只有兩個狀態:可運行狀態和等待狀態,在沒有任務時它們處於等待狀態,運行時可以循環地執行任務。

(2)任務接口(Task)

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

(3)任務隊列(Wok Queue)

也叫做工作隊列,用於存放等待處理的任務,一般是BlockingQueue的實現類,用來實現任務的排隊處理。

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


public class Executors{

public static ExecutorService newFixedThreadPool(int nThreads){

//生成一個最大為nThreads的線程池執行器

return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.

MILLISECONDS, new LinkedBlockingQueue<Runnable>());

}

}


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


public Future<?>submit(Runnable task){

//檢查任務是否為null

if(task==null)throw new NullPointerException();

//把Runnable任務包裝成具有返回值的任務對象,不過此時並沒有執行,只是包裝

RunnableFuture<Object>ftask=newTaskFor(task, null);

//執行此任務

execute(ftask);

//返回任務預期執行結果

return ftask;

}


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

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

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

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

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


private final class Worker implements Runnable{

//運行一次任務

private void runTask(Runnable task){

//這裡的task才是我們自定義實現Runnable接口的任務

task.run();

}

//工作線程也是線程,必須實現的run方法

public void run(){

while(task!=null||(task=getTask())!=null){

runTask(task);

task=null;

}

}

//從任務隊列中獲得任務

Runnable getTask(){

for(;){

return workQueue.take();

}

}


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


public E take()throws InterruptedException{

//如果隊列中元素數量為0,則等待

while(count.get()==0)

notEmpty.await();

//等待狀態結束,彈出頭元素

x=extract();

//如果隊列數量還多於1個,喚醒其他線程

if(c>1)

}

notEmpty.signal();

//返回頭元素

return x;

}


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

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