在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容器,每次請求處理的都是一個線程,如果不採用線程池技術,每次請求都會重新創建一個線程,這會導致系統的性能負荷加大,響應效率下降,降低了系統的友好性。