深入併發之(四) 執行緒池詳細分析

qq_42606051發表於2018-09-14

執行緒池的分類

首先我們需要了解,使用執行緒池的目的。如果我們有大量的較短的非同步任務需要執行,那麼我們需要頻繁的去建立執行緒並關閉執行緒。那麼這樣做的代價是十分巨大的,因此,我們就採用了一種執行緒池的做法,實際上,我們常用了池類方式還有資料庫連線池,這種一般是將一些比較珍貴的資源放在池中,然後,每次使用完畢,再將其放回池中,不釋放。節約了新建的成本。

下圖是執行緒池的簡單類圖

執行緒池類圖

一般我們通過Executors這個工廠類來建立執行緒池,那麼,我們來看一下,幾種執行緒池的真面目吧!

對於執行緒池中使用的任務佇列的資料結構,之後會單獨開部落格分析,這裡先有一個簡單的認識就好

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

固定大小的執行緒池,執行緒池的基本大小(corePoolSize)為nThreads,最大執行緒數(maximumPoolSize)也為nThreads,採用了LinkedBlockingQueue佇列來存放任務。

LinkedBlockingQueue是基於連結串列結構的阻塞佇列,FIFO,佇列屬於無界佇列。

當執行緒池中的執行緒數量小於最大執行緒數量時,會直接新建執行緒,執行任務。當執行緒數量已經達到最大執行緒數量,並且,沒有空閒執行緒,任務會進入佇列中排隊,當有空閒執行緒則會執行。

keepAliveTime執行緒活動時間,這個值表示,當執行緒池中的執行緒大於corePoolSize時,超出corePoolSize的空閒執行緒最大存活時間,當時間超過keepAliveTime,執行緒將會結束,當這個值為0表示,執行緒不會被回收。

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

無界的執行緒池,並且執行緒空閒達到一定時間,會被回收,這裡設定的時間是60s。

採用的佇列是SynchronousQueue,不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態。

因此,對於這種執行緒池,當有任務時,如果沒有空閒執行緒,會直接增加執行緒,執行任務,由於我們將最大執行緒數設定為Integer.MAX_VALUE,所以,執行緒池中的執行緒理論上沒有上線,但是機器的效能是有限的,所以如果任務非常多,可能會發生資源耗盡的情況。

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

單執行緒的執行緒池,一般情況下,當我們有很多工需要執行,但是他們並不需要同時執行,或者是有依賴關係的時候,例如B任務必須在A任務之後執行時,我們可以使用newSingleThreadPool

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

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

這個執行緒池一般用來執行定時任務,一般用下面的方法來設定定時任務執行的頻率。

pool.scheduleWithFixedDelay(task, 2, 3, TimeUnit.SECONDS);

上面的方法表示,任務task在提交後2秒開始執行,執行完畢後每3秒執行一次。

  • newWorkStealingPool
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

這個執行緒池是在Java8中新增的執行緒池,可以看出,這個執行緒池實際上是ForkJoinPool,這裡就是採用了fork join演算法。當一個任務可以分解為多個小任務的時候,我們可以使用這種方式,充分利用CPU的效能。

但是,我們需要注意的是,如果任務十分簡單,那麼這種拆分方式不僅不會提高效率,有時因為執行緒切換結果合併等問題可能還會更慢。

分析執行緒池的submit方法

MyCallable callable = new MyCallable();

ExecutorService executorService = Executors.newFixedThreadPool(5);

Future<String> future executorService.submit(callable);

String s = future.get();

System.out.println(s);

executorService.shutdown();

這裡我們分析的實際上就是上面這段程式碼中的submit方法。這裡我們注意ExecutorServiceAbstractExecutorService的子類,ExecutorService中沒有重寫方法submit,那麼我們呼叫的實際是父類的方法。

/**
 * @throws RejectedExecutionException {@inheritDoc}
 * @throws NullPointerException       {@inheritDoc}
 */
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

這裡我們看到了在上一篇深入併發之(三) FutureTask與Future之寫執行緒池的時候到底需不需要自己包裝FutureTask - 菱靈心 - 部落格園中介紹過的方法newTaskFor,作用是將Callable包裝為FutureTask,這裡不再介紹。

我們可以看到這個方法是直接返回的,這裡也對應了第一部分提到的,這種future設計方法的核心,就是無需等待非同步任務執行,我們可以讓主執行緒先去做其他任務,稍後我們可以從返回值中獲取我們想要的結果。

大致講解執行緒池中一些基本內容

執行緒數與執行狀態的儲存

需要注意,這一部分的主要內容實際是為了方便下面來檢視原始碼。

首先是ThreadPoolExecutor類。

對於執行緒池,我們肯定需要知道執行緒池中現在有多少執行緒,同時我們也需要知道執行緒池的狀態。那麼,在ThreadPoolExecutor中是如何儲存這個資料的呢。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

執行緒池中採取了原子的Integer,並且將32位的整數分為兩部分,分別儲存runstateworkercount,這種儲存資料的方式不是我們第一次見到了,在AQS中的共享鎖中也是類似的方式,原始碼中經常使用的一種寫法。workercount被儲存在高3位中,餘下的低位用來儲存runstate

執行狀態的變化

這裡我們通過一張圖來了解執行緒執行狀態的改變過程。

執行緒池執行狀態

圖中上面是執行緒池的狀態,下面是代表狀態的常量值。當執行緒池剛剛建立的時候狀態是RUNNING。

分析execute方法

下面進入正題,執行方法。

首先描述一下方法,這個方法主要分為3步:

  • 如果執行緒數量少於corePoolSize,那麼我們直接建立一個新的執行緒來執行任務,通過呼叫方法addWorker來檢查runstateworkercount,如果無法新增執行緒,那麼這個方法將會返回false。
  • 如果任務成功進入佇列,我們依然需要double-check是否需要增加一個執行緒(因為可能有一些執行緒在上次檢查之後死亡),或者執行緒池的狀態已經改變。所以通過二次檢查讓我們來確定是否需要回滾佇列或者增加執行緒。
  • 最後如果插入佇列失敗,我們再次嘗試新增執行緒執行任務,如果無法新增那麼應該是線上程池關閉或者飽和了,執行拒絕策略

給出流程圖

execute流程圖

    //這段程式碼需要著重注意幾處呼叫addWorker的區別,引數的不同,後面分析完addWorker再回頭看才會真正發現呼叫的目的
    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);
    }

對應上面的解釋與流程圖,這段程式碼的基本內容十分容易理解。

分析addWorker

在分析addWorker方法之前,我們先來看ThreadPoolExecutor類的內部類Worker

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable

當我們執行的時候實際會把Runnable再包裝為Worker,通過firstTask。

 /** Thread this worker is running in.  Null if factory fails. */
 final Thread thread;
 /** Initial task to run.  Possibly null. */
 Runnable firstTask;
 /** Per-thread task counter */
 volatile long completedTasks;

同時,還實現了一個獨佔鎖,這部分程式碼就是應用了之前講到的利用AQS實現獨佔鎖的方法。如果不瞭解可以參照博主的另外兩篇文章。

深入併發之(一) 從來ReentrantLock看AbstractQueuedSynchronizer原始碼 - 菱靈心 - 部落格園

兩種方式實現自己的可重入鎖 - 菱靈心 - 部落格園

這裡不詳細講解,只給出程式碼。

protected boolean tryAcquire(int unused) {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
}

public void lock()        { acquire(1); }
public boolean tryLock()  { return tryAcquire(1); }
public void unlock()      { release(1); }
public boolean isLocked() { return isHeldExclusively(); }

下面,我們來看一下其中的重點方法addWorker

這段程式碼主要可以分為兩部分,第一部分是檢查執行緒池的狀態,如果不滿足條件會直接返回false,然後進入死迴圈等待成功增加執行緒,如果增加執行緒成功,那麼就可以進入第二部分,真正新增執行緒,執行任務。具體的邏輯可以參考程式碼註釋。這裡給出一個簡要的流程圖。

addWorker流程圖

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            //執行緒池狀態
            int rs = runStateOf(c);

            /**
             * 大於等於SHUTDOWN表示執行緒池已經不應該接受新的任務了,自然應該返回false。
             * 但是這裡有一個前提就是需要清空已經進入佇列的任務。
             */
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                //執行緒數
                int wc = workerCountOf(c);
                /**
                 * 由於位數限制,執行緒池有一個CAPACITY,所以超出的不能建立執行緒了。
                 * 同時,還有核心執行緒和最大執行緒數,一般來說執行緒池中可以建立的執行緒數在不會超過最大執行緒數,
                 * 這裡通過那個控制主要取決於傳入的引數,同時也受建立執行緒池時設定的最大執行緒數的控制。
                 * 可以參考上面的執行緒池分類的部分檢視幾種執行緒池的最大執行緒數。
                 */
              
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                /**
                 * 上面的關卡都過去了,終於可以嘗試增加執行緒數了,這裡實際是CAS操作,
                 * 如果不瞭解,可以到網上搜尋,或者參考博主AQS那篇
                 * 建立成功會直接跳出死迴圈,進入第二部分,執行任務
                 */
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                /**
                 * 重新檢視執行緒池的狀態,如果狀態改變,那麼就需要重新進行上面的狀態部分的判斷,
                 * 需要跳出這個等待增加執行緒數的死迴圈
                 */
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        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) {
                    //增加成功就可以執行了,實際呼叫了Worker的run方法
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //如果沒有執行,那麼有些內容需要回滾
                addWorkerFailed(w);
        }
        return workerStarted;
    }

到這裡為止,我們對執行緒池的基本工作原理有了認識,已經深入分析了原始碼。但是這裡我們還需要提出一個問題,我們將任務加入到佇列中後,到底執行緒池是在什麼時候由那個方法將其從佇列中取出,進行執行的呢?其實這裡流程圖中已經提到了,當我們將任務加入佇列後,會呼叫addWorker方法,但是並沒有傳入任務,這裡實際上會到佇列中取任務,那麼取任務的程式碼在哪裡呢?實際上是在Worker類中的run方法,Worker類是一個Runnable介面的實現類,在addWorker方法中呼叫的t.start();實際呼叫了Worker的run方法,下一篇部落格中,我們將會介紹這個方法中實現的功能。

來源:https://www.cnblogs.com/qmlingxin/p/9638253.html

鄭州看不孕不育哪家好

鄭州不孕不育醫院

鄭州不孕不育醫院

鄭州不孕不育醫院

相關文章