建議125:優先選擇線程池
在Java1.5之前,實現多線程比較麻煩,需要自己啟動線程,并關注同步資源,防止出現線程死鎖等問題,在1.5版本之后引入了并行計算框架,大大簡化了多線程開發。我們知道一個線程有五個狀態:新建狀態(NEW)、可運行狀態(Runnable,也叫作運行狀態)、阻塞狀態(Blocked)、等待狀態(Waiting)、結束狀態(Terminated),線程的狀態只能由新建轉變為了運行狀態后才能被阻塞或等待,最后終結,不可能產生本末倒置的情況,比如把一個結束狀態的線程轉變為新建狀態,則會出現異常,例如如下代碼會拋出異常:
public static void main(String[] args) throws InterruptedException {
// 創建一個線程,新建狀態
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程正在運行");
}
});
// 運行狀態
t.start();
// 是否是運行狀態,若不是則等待10毫秒
while (!t.getState().equals(Thread.State.TERMINATED)) {
TimeUnit.MICROSECONDS.sleep(10);
}
// 直接由結束轉變為云心態
t.start();
}
此段程序運行時會報java.lang.IllegalThreadStateException異常,原因就是不能從結束狀態直接轉變為運行狀態,我們知道一個線程的運行時間分為3部分:T1為線程啟動時間,T2為線程的運行時間,T3為線程銷毀時間,如果一個線程不能被重復使用,每次創建一個線程都需要經過啟動、運行、銷毀時間,這勢必增大系統的響應時間,有沒有更好的辦法降低線程的運行時間呢?
T2是無法避免的,只有通過優化代碼來實現降低運行時間。T1和T2都可以通過線程池(Thread Pool)來縮減時間,比如在容器(或系統)啟動時,創建足夠多的線程,當容器(或系統)需要時直接從線程池中獲得線程,運算出結果,再把線程返回到線程池中___ExecutorService就是實現了線程池的執行器,我們來看一個示例代碼:
public static void main(String[] args) throws InterruptedException {
// 2個線程的線程池
ExecutorService es = Executors.newFixedThreadPool(2);
// 多次執行線程體
for (int i = 0; i < 4; i++) {
es.submit(new Runnable() {
@Override
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個線程可以反復使用呢?這就是我們要搞清楚的重點。
線程池涉及以下幾個名詞:
- 工作線程(Worker):線程池中的線程,只有兩個狀態:可運行狀態和等待狀態,沒有任務時它們處于等待狀態,運行時它們循環的執行任務。
- 任務接口(Task):這是每個任務必須實現的接口,以供工作線程調度器調度,它主要規定了任務的入口、任務執行完的場景處理,任務的執行狀態等。這里有兩種類型的任務:具有返回值(異常)的Callable接口任務和無返回值并兼容舊版本的Runnable接口任務。
- 任務對列(Work Quene):也叫作工作隊列,用于存放等待處理的任務,一般是BlockingQuene的實現類,用來實現任務的排隊處理。
我們首先從線程池的創建說起,Executors.newFixedThreadPool(2)表示創建一個具有兩個線程的線程池,源代碼如下:
public class Executors {
//生成一個最大為nThreads的線程池執行器
public static ExecutorService newFixedThreadPool(int 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() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
// 任務隊列中獲得任務
Runnable getTask() {
/* 其它代碼略 */
for (;;) {
return r = workQueue.take();
}
}
}
此處為示意代碼,刪除了大量的判斷條件和鎖資源。execute方法是通過Worker類啟動的一個工作線程,執行的是我們的第一個任務,然后改線程通過getTask方法從任務隊列中獲取任務,之后再繼續執行,但問題是任務隊列是一個BlockingQuene,是阻塞式的,也就是說如果該隊列的元素為0,則保持等待狀態,直到有任務進入為止,我們來看LinkedBlockingQuene的take方法,代碼如下:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
try {
// 如果隊列中的元素為0,則等待
while (count.get() == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to a non-interrupted thread
throw ie;
}
// 等待狀態結束,彈出頭元素
x = extract();
c = count.getAndDecrement();
// 如果隊列數量還多于一個,喚醒其它線程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
// 返回頭元素
return x;
}
分析到這里,我們就明白了線程池的創建過程:創建一個阻塞隊列以容納任務,在第一次執行任務時創建做夠多的線程(不超過許可線程數),并處理任務,之后每個工作線程自行從任務對列中獲得任務,直到任務隊列中的任務數量為0為止,此時,線程將處于等待狀態,一旦有任務再加入到隊列中,即召喚醒工作線程進行處理,實現線程的可復用性。
使用線程池減少的是線程的創建和銷毀時間,這對于多線程應用來說非常有幫助,比如我們常用的Servlet容器,每次請求處理的都是一個線程,如果不采用線程池技術,每次請求都會重新創建一個新的線程,這會導致系統的性能符合加大,響應效率下降,降低了系統的友好性。
省略了很多東西,因為有一些東西現在對于自己來說還不是那么實用,后邊還有幾個章節的內容也沒有整理,是因為感覺是一些更加廣泛的東西。有一些東西仍然很有針對性,但是在這里就不給出了。
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料