《編寫高質量代碼》學習筆記(3)

建議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學習資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容