Java/Android中的線程池,看這一篇就夠了!(超詳細)

一、爲何要使用線程池

在Java中,要使用多線程,除了使用new Thread()之外,還可以使用線程池ExecutorService

// 使用Thread
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        // ...
    }
});
t.start();

// 使用線程池
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new Runnable() {
    @Override
    public void run() {
        // ...
    }
});

線程池主要解決了兩個問題:

  1. 頻繁創建銷燬線程的開銷
  2. 任務的管理

在異步任務比較多時,創建、銷燬線程會佔用很多系統資源;這時候,使用線程池,就可以實現線程的複用,讓人專注於任務的實現,而不是管理線程。

二、線程池簡介

1. 什麼是線程池

線程池(本文特指ThreadPoolExecutor類)顧名思義,就是一個裝了線程的池子。線程池創建和管理若干線程,在需要使用的時候可以直接從線程池中取出來使用,在任務結束之後閒置等待複用,或者銷燬。
線程池中的線程分爲兩種:核心線程和普通線程。核心線程即線程池中長期存活的線程,即使閒置下來也不會被銷燬,需要使用的時候可以直接拿來用。而普通線程則有一定的壽命,如果閒置時間超過壽命,則這個線程就會被銷燬。

查看ThreadPoolExecutor類的其中一個典型的構造方法:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

線程池的具體行爲和幾個參數有關:

  • 核心數 corePoolSize
    線程池中核心線程的數量。
  • 最大容量 maximumPoolSize
    線程池最大允許保留多少線程。
  • 超時時間 keepAliveTime
    線程池中普通線程的存活時間。

2. 線程池的使用

線程池的一般使用步驟如下:

  1. 使用Executors中的工廠方法來獲取ExecutorService實例;
  2. 使用ExecutorServiceexecute(runnable)或者submit(runnable)方法來添加任務。
    ExecutorService es = Executors.newSingleThreadExecutor();
    es.execute(new Runnable() {
        @Override
        public void run() {
            String response = new HttpUtil().get("http://littlefogcat.top");
            System.out.println(response);
        }
    });

3. 線程池的分類

Executors工廠類中提供了多種線程池,典型的有以下四種:

1. SingleThreadExecutor 單線程線程池

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                     0L, TimeUnit.MILLISECONDS,
                     new LinkedBlockingQueue<Runnable>()));
    }

核心線程數爲1,最大線程數爲1,也就是說SingleThreadExecutor這個線程池中的線程數固定爲1。使用場景:當多個任務都需要訪問同一個資源的時候。

2. FixedThreadPool 固定容量線程池

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

核心線程數爲n,最大線程數爲n。使用場景:明確同時執行任務數量時。

3. CachedThreadPool 緩存線程池

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

核心線程數爲0,最大線程數無上限,線程超時時間60秒。使用場景:處理大量耗時較短的任務。

4. ScheduledThreadPool 定時線程池

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

/*
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
*/

核心線程數自定,最大線程數無上限。使用場景:處理延時任務。

可以看到,這四個方法都返回了一個ThreadPoolExecutor對象(ScheduledThreadPoolExecutor是其子類),僅僅是其中的參數略有不同。所以接下來就對ThreadPoolExecutor類進行解析。

我將這四種常見的線程池總結了一個表格:
線程池類型

三、線程池的工作流程

1. 典型的線程池使用方式

一個典型的線程池使用方式如下:

ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Runnable() {
        @Override
        public void run() {
            // do sth
        }
});

這裏就以ThreadPoolExecutor.execute(runnable)方法切入,分析線程池的工作流程。
ThreadPoolExecutor.execute(runnable)方法的註釋中寫道:

Executes the given task sometime in the future. The task
may execute in a new thread or in an existing pooled thread.
If the task cannot be submitted for execution, either because this
executor has been shutdown or because its capacity has been reached,
the task is handled by the current {@code RejectedExecutionHandler}.

簡單來說就是將這個傳入的runnable對象提交到線程池中,等待執行;如果線程池關閉,或者容量到上限不可以執行了,那麼就無法提交,會交給線程池的RejectedExecutionHandler進行處理(這個RejectedExecutionHandler在構造方法中傳入,或者通過setRejectedExecutionHandler(handler)方法指定)。

2. 線程池工作流程

線程池的工作流程還是比較清晰的,具體的源碼分析在第四節中,本節只做簡要說明。

2.1 添加任務

當調用ThreadPoolExecutor.execute(runnable)的時候,會進行以下判斷(這裏不考慮延時任務):

  1. 如果線程池中,運行的線程數少於核心線程數(corePoolSize),那麼就新建一個線程,並執行該任務。
  2. 如果線程池中,運行的線程數大於等於corePoolSize,將線程添加到待執行隊列中,等待執行;
  3. 如果2中添加到隊列失敗,那麼就新建一個非核心線程,並在該線程執行該任務;
  4. 如果當前線程數已經達到最大線程數(maximumPoolSize),那麼拒絕這個任務。

這裏有個問題,什麼情況下,任務會添加失敗呢?這個問題會在下面第四節源碼分析中workQueue部分說明。

2.2 執行任務

在2.1添加任務中,添加失敗自然不必執行,會直接拒絕任務;任務添加成功有兩種情況:

  • 將任務添加到任務隊列;
  • 新建線程執行任務。

新建線程自不必說,主要看看添加到任務隊列中的任務是如何被執行的。
從2.1中我們知道,每一個工作線程必然是被一個任務喚醒的,這個任務被稱作初始任務(firstTask)。當一個工作線程完了它的初始任務之後,會從待執行的任務隊列(workQueue)中取新的任務。workQueue是一個阻塞隊列,線程會一直等待直到有新的任務到來爲止。對於一個設置了超時時間的線程,如果在指定的時間之後仍然沒有新任務到達,那麼這個線程就會停止等待任務並且銷燬。

四、線程池中的一些重要概念

Worker / workers

Worker類是ThreadPoolExecutor類的一個內部類,也是線程池管理操作線程的核心所在。每一個worker都對應着一個thread,所以在不混淆的情況下,可以把worker理解爲工作線程。

ThreadPoolExecutor有一個名爲workers的成員變量,它保存了這個線程池所有存活的worker對象。

workQueue

workQueue是線程池內部用來保存待執行任務的隊列。它是一個BlockingQueue<Runnable>類型的變量,在沒有任務的時候,它的poll()方法會阻塞。

在一個允許超時的worker執行完任務之後,會調用workQueue.poll()取出下一個任務執行。如果沒有任務,則會在這裏阻塞;當阻塞時間達到超時時間後,這個工作線程會退出並銷燬。

五、通過源碼詳細分析線程池

1. ctl

ThreadPoolExecutor通過一個原子整型ctl來保存線程池的兩個重要字段,workerCount和runState。workerCount即線程池工作線程的數量,而runState代表了線程池當前的狀態(如:運行中、關閉、終止)。通過位運算,可以從ctl得到workerCount和runState的值,反之也可以通過workerCount和runState組合得到ctl

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

顯然,這跟Android中的MesureSpec通過一個整數來保存兩個屬性原理是相同的。

2. execute(runnable)方法

本節所有流程都以ThreadPoolExecutor.execute(runnable)方法切入,分析線程池的源碼:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

可以看到,這個方法很簡單,正如三-2.1小節所說的一樣,在添加任務時做一些判斷。在ThreadPoolExecutor中,有一個隊列workQueue保存了待執行的任務。而當需要新建線程的時候,則執行addWorker(runnable, core)方法來創建一個worker/線程。

因爲這個方法是線程池執行的核心,所以下面重點理解這個方法裏面的語句。

3. workQueue / Worker

workQueueThreadPoolExecutor類的一個非常重要的成員變量。在2中,我們知道了,當正在執行的線程數量大於核心線程數,那麼會優先將任務添加到任務隊列,即workQueue中。

通過execute(runnable)方法可以知道,對於一個處在運行中的線程池,只有在當前工作線程數量大於等於核心數時,纔會將任務往隊列中添加。並且,如果往任務隊列添加失敗的話,就會開啓新的工作線程。

那麼回到第三節中的問題,什麼情況下會添加失敗呢?注意這一句:

if (isRunning(c) && workQueue.offer(command)) {
    // ...
}

很簡單,當workQueue.offer(command)返回false的時候,則說明添加失敗。一般來說,當隊列的容量滿了,offer方法就會返回false。**即,在線程數超過了核心數(workerCount>corePoolSize)的情況下,只有在任務隊列被填滿之後,線程池纔會考慮創建新線程,否則只會將任務添加到任務隊列中等待執行。**在線程池的構造方法中傳入不同的隊列類型,就會有不同的效果。回到Executors工廠類中,看看四種基本的線程池分別都是使用的什麼隊列?

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    // ScheduledThreadPoolExecutor的構造方法
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  • SingleThreadExecutor
    核心數和最大線程數均爲1,使用LinkedBlockingQueue,容量爲Interger.MAX_VALUE。也就是說,SingleThreadExecutor中永遠只有一個線程,所有任務單線執行,並且容量無上限。

  • CachedThreadPool
    核心數爲0,最大線程數Interger.MAX_VALUE,使用SynchronousQueue。這個隊列的特點是,沒有內部容量。也就是說,對於一個新任務,但凡是沒有空閒的線程,那麼就創建一個新的線程。而由於核心數是0,當超過一定時間沒有新任務之後,線程池中所有線程都將被銷燬。

  • FixedThreadPool
    和SingleThreadExecutor類似,使用LinkedBlockingQueue;不同的是核心數和最大線程數爲n。

  • ScheduledThreadPoolExecutor
    使用DelayedWorkQueue,可以實現延時/定時獲取任務。

看完這裏,就能很好的理解Executors中的這些線程池爲何能夠呈現出各自的特性了。

在第四節中我們知道,對於線程的操作等,不是直接通過Thread來進行的,而一般是通過Worker類進行。每一個Worker對應了一個線程,任務的添加、執行等,都是通過Worker來實現的。ThreadPoolExecutor中有一個HashSet<Worker>類型的變量workers,用來保存可用的Worker。也就是說,我們所謂的“線程池”實際本質上就是“Worker池”。由於WorkerThread是一對一的關係,所以爲了圖方便,有時候可以簡單的把Worker理解成一個工作線程,但需要知道其本質上與真正的線程Thread是不同的。

Worker類是ThreadPoolExecutor的一個內部類,繼承自AbstractQueuedSynchronizer,實現了Runnable接口:

    // ...略去一部分
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        final Thread thread;
        Runnable firstTask;

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
            runWorker(this);
        }

    }

真實運行的線程,是worker.thread,在Worker構造方法中,worker.thread通過工廠方法創建。而線程肯定是要調用start()方法運行的,搜索一下worker.threadstart()方法,發現是在ThreadPoolExecutor.addWorker()這個方法裏調用的。
在下面的第4小節中,會專門分析這個addWorker(runnable, core)方法。

另一方面,Worker本質上又是一個Runnable對象,是一個可運行任務,在真實線程worker.thread啓動後,會調用其run()方法:

        // Worker中
        public void run() {
            runWorker(this);
        }

4. addWorker(runnable, boolean)方法

線程池創建工作線程是通過addWorker方法來進行的。

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

雖然這段代碼有點長,但是所做的事情其實只有兩件:

  1. 檢查是否應該添加這個worker:只有在線程池處於正在運行的狀態(runState==RUNNING),並且當前worker數小於最大容量時,才能添加;
  2. 新建worker對象並添加到workers中。

5. runWorker(Worker)方法

在3中我們得知,當Worker的線程開始運行之後,會調用其run()方法:

        // Worker中
        public void run() {
            runWorker(this);
        }

而run()又會調用ThreadPoolExecutor.runWorker(Worker)方法。在這裏看一下這個方法。

    // 省略一大部分
    final void runWorker(Worker w) {
        Runnable task = w.firstTask;
        while (task != null || (task = getTask()) != null) {
            try {
                task.run();
            } catch (Exception x) {
            } 
        }
    }

    // 省略一大部分
    private Runnable getTask() {
        for (;;) {
            if (/*無法獲取任務*/) {
                return null;
            }
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
            } catch (InterruptedException retry) {
            }
        }
    }

爲了便於觀看,我刪去了大部分代碼,只留了核心的幾行。可以看到,在Worker的任務執行完畢之後,會再從workQueue隊列中獲取新的任務,按此無限循環。什麼時候Worker會結束並銷燬呢?從這一句while (task != null || (task = getTask()) != null)中,即worker中沒有任務,並且getTast()返回null,worker就會結束執行。什麼時候返回null,不讓worker繼續存活了呢?

  • 線程池被shutdown,並且任務隊列空了;
  • 線程池超容量;
  • 超時;

也就是說,如果線程池在運行狀態,容量也沒有到最大,並且任務隊列還有任務,這個worker就會永遠運行下去。

六、總結

就用圖片來總結一下。

下圖闡述了線程池調用execute(runnable)之後的流程。
線程池.png

這張圖表示了execute之後的調用鏈,相當於Worker的生命週期了(不包括銷燬)。

線程池2.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章