Java中ThreadPool的原理與實現

(1)爲什麼需要ThreadPool?

當我們在使用ThreadPool的時候,首先要明白爲什麼需要ThreadPool?ThreadPool中到底有些什麼?

我們知道進程代表程序的一次執行。進程在創建過程中會加載可執行文件到內存(爲了提高執行效率,一般是將可執行文件映射到進程的地址空間,進行lazy load),然後由線程執行可執行文件中的指令。多個進程可以執行同一個可執行文件。所以一個進程至少要有一個線程,一般稱之爲主線程。進程擁有自己的地址空間,進程中的線程則共享進程的地址空間,所以多個線程可以訪問進程中定義的共享變量。這就是在多線程環境下爲什麼需要互斥保護共享變量的原因。

隨着硬件的發展,電腦中的CPU配置也從一個變成了多個,從單核變成了多核。所以可以通過在程序中創建多個線程的方式來充分利用CPU的運算能力。在一個4核CPU上同時執行4個線程,比只執行1個線程對CPU的利用率要高很多。同時,多線程也可以大大提高用戶的體驗。比如在GUI應用中,主線程主要用來進行圖形用戶界面的刷新及響應用戶的各種操作,而同時還會有多個工作線程負責進行運算或者加載數據。如果只有一個主線程,那麼可想而知用戶的體驗會有多麼的差。當從網絡上加載數據的時候,因爲主線程的阻塞將導致真個程序無法響應用戶的任何輸入。

既然多個線程的程序已經可以很好的運行,爲什麼還需要ThreadPool呢?答案很簡單:提高執行效率。我們知道,當從程序中調用API創建一個線程的時候,會進行一次系統調用從而從用戶態進入內核態。在內核中,操作系統會創建線程的管理單元,設置好線程的執行環境然後再切換到用戶態執行線程函數。在線程銷燬時,會再一次進入內核態,由操作系統銷燬相應的管理單元及相關資源。所以說線程的創建與銷燬是一件非常費時的操作。那麼可不可以重用創建好的線程從而避免多次創建、銷燬?答案就是ThreadPool。所以ThreadPool就是線程的管理器,當我們需要執行一個任務的時候,直接告訴ThreadPool,由ThreadPool選擇一個線程來執行。一個thread pool的兩個重要組成部分是:線程組和任務隊列。

(2)Java中的ThreadPool

下面通過一個一個例子來看一下在Java中如何使用thread pool。首先需要定義一個task以讓thread pool執行,代碼如下:

public class Task implements Runnable {
    private String name;
    public Task(String name){
        this.name = name;
    }
    @Override
    public void run() {
        Long delay = (long)Math.random() * 10;
        try {
            TimeUnit.SECONDS.sleep(delay);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Task:" + name);
    }
}

接下來,我們就可以使用Executors工具類來創建thread pool並運行上面定義的task。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {
    public static void main(String[] args){
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++){
            executor.execute(new Task("Task:" + i));
        }
        executor.shutdown();
    }
}

上面的代碼創建了一個thread pool,代碼執行的結果如下:

Task:Task:4
Task:Task:1
Task:Task:0
Task:Task:2
Task:Task:3

執行多次,會得到不同的執行結果。這也充分說明線程的執行順序是不確定的。如果線程之間存在依賴,需要使用同步機制來保證線程的執行次序。

在Executors類中,定義了幾種生成不同thread pool的方法,主要由以下幾個:

  • public static ExecutorService newCachedThreadPool() //創建一個線程數動態調整的線程池,注意:線程數量無上限
  • public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) //同上,使用提供的線程工廠創建線程
  • public static ExecutorService newFixedThreadPool(int nThreads) //線程池中線程數量固定爲nThreads
  • public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
  • public static ScheduledExecutorService newSingleThreadScheduledExecutor() //創建只包含一個線程的可定時線程池
  • public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) //線程池中至少包含corePoolSize個線程
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

從接口定義可以看出,線程池主要分爲兩大類:可定時執行線程池(ScheduledExecutorService)和不可定時執行線程池(ExecutorService)。

對於ExecutorService,有兩個主要的接口用來執行任務:

  • void execute(Runnable command)
  • <T> Future<T> submit(Callable<T> task)
  • <T> Future<T> submit(Runnable task, T result)
  • Future<?> submit(Runnable task)


execute和submit方法的主要區別是任務是否可控。在execute方法中,一個任務提交之後,只能等待任務完成,要麼執行成功,要麼失敗。而在submit方法中,會返回一個Future對象,通過該對象可以嘗試取消該任務及查詢任務是否完成。

ScheduledExecutorService繼承至ExecutorService,因此除了上面介紹的方法外,有另外幾個可以用於定時的方法:

  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

(3)線程池的實現

知道了怎麼樣使用線程池,那麼另外一個重要的問題是線程池怎麼實現呢?爲了搞清楚這個問題,首先看一下線程池相關類之間的關係。

wKioL1dTgvCxGvmjAAE69Nx-HR4757.png-wh_50

ThreadPoolExecutor是ExecutorService的實現類,ScheduledThreadPoolExecutor繼承至ThreadPoolExecutor,是ScheduledExecutorService的實現類。我們以ThreadPoolExecutor爲例看一下線程池的具體實現。

ThreadPoolExecutor的主要字段由以下幾個:

  • private final BlockingQueue<Runnable> workQueue
  • private final ReentrantLock mainLock = new ReentrantLock();
  • private final HashSet<Worker> workers = new HashSet<Worker>(); //管理所有的工作線程
  • private final Condition termination = mainLock.newCondition(); //用於中斷線程
  • private volatile ThreadFactory threadFactory; //創建線程的工廠類
  • private volatile long keepAliveTime; //線程空閒回收超時時間
  • private volatile int corePoolSize; //線程池中基本線程數量
  • private volatile int maximumPoolSize; //最大線程數
  1. 線程池的狀態

    一個線程池處於下面四中狀態之一:

    1. Running: 可以接受新的任務和處理已經在隊列中的任務

    2. Shutdown: 不接受新的任務,但會處理已經在隊列中的任務

    3. Stop: 不接受新的任務,也不處理隊列中的任務,並會中斷運行中的任務

    4. Tidying: 所有的任務都已經中止,工作線程數量爲0. 在轉換到Tidying狀態時,會調用terminated()鉤子方法

    5. Teriminated: 調用terminated()方法後所處的狀態

線程池狀態之間的轉換如下圖:

wKiom1dVz9_w7wDoAABsa7fKrto872.png

線程池創建後默認是處於Running狀態。shutdown()會使線程池進入shutdown狀態,而shutdownNow()則會立即進入到stop狀態。

線程池的狀態在代碼中定義如下:

private static final int COUNT_BITS = Integer.SIZE - 3;
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

有意思的是在Java線程池的實現中,狀態與工作線程的數量放在了同一個字段中,而使用不同的位來表示狀態和工作線程的數量。從上面的代碼可以看出,狀態使用了32位中最高3位保存,而低29位用於記錄工作線程的數量。一些輔助的函數如下:

// Packing and unpacking ctl
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; }

任務的提交與執行當通過execute(Runnable command)提交任務時,會通過以下3步進行處理:(a)如果線程池中的線程數量少於corePoolSize,創建一個新的線程,並將當前任務直接賦予新創建的線程進行執行(b)如果線程池的線程數量大於corePoolSize,將嘗試將任務放入任務隊列(workQueue)中。如果成功放入到任務隊列中,將會檢查線程池的狀態。如果線程池正常運行,但線程池中的線程數量爲0,將創建新的線程執行隊列中的任務(c)如果線程池的線程數量大於corePoolSize,同時將任務放入任務隊列失敗時,將嘗試創建新的線程執行提交的任務如果任務不能將任務放入任務隊列,或者無法添加新的線程處理任務,則會拒絕該任務。默認是丟棄該任務。ThreadPoolExecutor定義了一個內部類Worker來實現對線程的封裝private final class Worker     extends AbstractQueuedSynchronizer     implements RunnableWorker中兩個主要的字段是:final Thread thread; //當前worker對應的線程Runnable firstTask; //初始任務每當需要創建一個新的線程時,就生成一個新的Worker對象,同時將Worker對象加入到workers集合中。在Worker的構造函數中,通過ThreadFactory創建一個新的線程並與當前worker對象綁定。因爲Worker類實現了Runnable接口,所以當線程執行時,將執行Worker.run()方法,進而調用ThreadPoolExecutor.runWorker(Worker w)方法。在runWorker中,將處理firstTask或者從workQueue中取任務進行處理,所以相應的代碼在一個循環中。如果沒有任務可處理,則會縮減線程池中線程的數量。線程池的終止線程池可以通過shutdown()或者shutdownNow()兩個方法終止。shutdownNow()會直接使線程池轉換到stop狀態。線程池終止時,對於每個worker對象,調用線程的interrupt()終止線程的執行。

(4)總結

  1. 線程池適合大量的異步任務的執行

  2. Executors.newCachedThreadPool()創建的線程池沒有線程上限,所以有可能用盡系統中的所有資源

  3. 線程池中兩個主要部分是:線程集合管理和任務隊列管理

  4. 每個線程的主循環會從任務隊列中取任務並處理任務


至此,你是否瞭解了線程池是什麼以及怎麼實現的?如果你來寫線程池,你會怎樣做?

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