ThreadPoolExecutor的使用及原始碼分析

MrChen的成長之路發表於2018-09-14

ThreadPoolExecutor自己也經常使用,也看過幾次原始碼,但是原始碼具體執行流程在經過一段時間之後有些就變得模糊。所以還是在此記錄一下ThreadPoolExecutor原始碼中的關鍵點和自己對程式碼的理解。
在文章前面部分介紹一下ThreadPoolExecutor相關知識點、使用流程(該部分內容參考:https://www.jianshu.com/p/ae67972d1156,感謝其作者),在後面部分會著重分析原始碼執行的關鍵路徑和我對原始碼執行理解,從原始碼角度對前面部分的使用做一個對應。

一、ThreadPoolExecutor的基本介紹

1、構造方法中各引數的理解:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);
  • int corePoolSize:該執行緒池中核心執行緒數最大值。

核心執行緒:執行緒池新建執行緒的時候,如果當前執行緒總數小於corePoolSize,則新建的是核心執行緒,如果超過corePoolSize,則新建的是非核心執行緒。核心執行緒預設情況下會一直存活線上程池中,即使這個核心執行緒啥也不幹(閒置狀態)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut這個屬性為true,那麼核心執行緒如果不幹活(閒置狀態)的話,超過一定時間(時長下面引數決定),就會被銷燬掉。

  • int maximumPoolSize: 該執行緒池中執行緒總數最大值。
    執行緒總數 = 核心執行緒數 + 非核心執行緒數。

  • long keepAliveTime:該執行緒池中非核心執行緒閒置超時時長。
    一個非核心執行緒,如果不工作(閒置狀態)的時長超過這個引數所設定的時長,就會被銷燬掉,如果設定allowCoreThreadTimeOut = true,則會作用於核心執行緒。

  • TimeUnit unit:keepAliveTime的單位(直接參考TimeUnit 類中的定義)

  • BlockingQueue workQueue:該執行緒池中的任務佇列:維護著等待執行的Runnable物件。
    當所有的核心執行緒都在工作時,新新增的任務會被新增到這個佇列中等待處理,如果佇列滿了,則新建非核心執行緒執行任務。

  • ThreadFactory threadFactory:建立執行緒的介面,可以自己實現或者使用預設,我一般自己實現,可以修改預設執行緒名,使執行緒名和專案相關。

private static class ThreadFactory implements java.util.concurrent.ThreadFactory {
       private final AtomicInteger mCount = new AtomicInteger(1);
       @Override
       public Thread newThread(@NonNull Runnable r) {
           Thread result = new Thread(r, "my_thread" + "-" + mCount.getAndIncrement());
           result.setDaemon(false);
           return result;
       }
   }
  • RejectedExecutionHandler handler:拒絕策略,丟擲異常專用,一般不用自己定義。

2、各種任務佇列的使用策略:

  • SynchronousQueue:這個佇列接收到任務的時候,會直接提交給執行緒處理,不會儲存在工作佇列之中,當執行緒池中的執行緒數大於maximumPoolSize的時候就會丟擲異常。

  • LinkedBlockingQueue:這個佇列接收到任務的時候,如果當前執行緒數小於核心執行緒數,則新建執行緒(核心執行緒)處理任務。因為LinkedBlockingQueue佇列長度預設是Integer.MAX_VALUE,所以不會有非核心執行緒的存在,線上程數量達到核心執行緒數之後,每次都會直接將任務新增到該佇列之中,而不會啟動新的執行緒,所以maximumPoolSize是失效的。

  • ArrayBlockingQueue:可以限定佇列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了maximumPoolSize,並且佇列也滿了,則丟擲錯誤。

  • DelayQueue:佇列內元素必須實現Delayed介面,這就意味著你傳進去的任務必須先實現Delayed介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務。

二、從原始碼的角度分析ThreadPoolExecutor的使用

該部分著重以ThreadPoolExecutor結合LinkedBlockingQueue的使用分析執行緒池使用的關鍵路徑,本部分大部分以程式碼註釋的方式分析。
看原始碼之前丟擲問題:①新增任務的過程是怎麼樣的;②核心執行緒怎麼保持不被銷燬的;③核心執行緒、非核心執行緒、工作佇列之間的協調。

1、ThreadPoolExecutor中的幾個狀態和幾個方法:

	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    // 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;
   
	// 計算當前執行緒池的狀態
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    //計算當前執行緒的個數
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    //根據狀態值和執行緒數合成ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }
  • AtomicInteger型別常量ctl:用來儲存當前執行緒池狀態和當前Worker個數,它的初始值和二進位制表示為:-536870912(11100000000000000000000000000000)
  • CAPACITY:類似於掩碼,用來輔助計算狀態值和workerCount的,值:536870911(00011111111111111111111111111111)
  • RUNNING狀態:該狀態的執行緒池會處理所有的任務,包括新來的和阻塞佇列中的,值:-536870912(11100000000000000000000000000000)
  • SHUTDOWN狀態:該狀態的執行緒池不會再接收新任務,但還會處理已經提交到阻塞佇列中等待處理的任務,值:0
  • STOP狀態:不會再執行任何任務,且中斷正在執行的任務,值:536870912(00100000000000000000000000000000)

從上面可以看出,執行緒池設計上是以一個int型別的值的前3位表示當前執行緒池的狀態,後面的位數標識當前正在執行的任務的個數(Worker)。

2、從execute()方法開始:
從該方法看出執行緒池新建執行緒執行任務的策略:
①當前執行緒數小於核心執行緒數的時候直接新建執行緒執行任務;
②如果佇列能夠新增任務,則優先將任務新增到佇列,而不是新建程式;
③在佇列已滿的時候,如果執行緒數小於最大執行緒數,則新建非核心執行緒執行任務,否則報錯。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        int c = ctl.get();
        //當前正在工作的執行緒小於核心的執行緒數,跳轉到addWorker()方法,新建執行緒執行任務
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //大於等於核心執行緒數:當前執行緒在執行狀態,且新增到任務佇列成功了。
        //對於LinkedBlockingQueue佇列,新增任務總會成功(它的任務佇列長度很大),所以程式碼走這裡,
        //對於SynchronousQueue因為新增失敗(不能新增任務到佇列),所以不會從這個分支走。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //重新檢查狀態,可能這個時候執行緒池被停止了,要remove新新增的任務
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //判斷當前執行緒數是不是0,如果是,則新建一個執行緒執行新加到佇列裡面的任務,否則會沒有執行緒去執行新新增進阻塞佇列的任務(command)。
            //如果當前執行緒數大於0,則不需要新增,因為有執行緒在執行,線上程執行完成之後,後喚醒阻塞佇列裡面的任務,新加到阻塞佇列裡的任務還是能夠被執行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //對於SynchronousQueue會走到這裡,所以,線上程數小於最大執行緒數的時候,會新建執行緒執行任務,當大於最大執行緒數時,會直接報錯reject(),此時addWorker()會失敗
        else if (!addWorker(command, false))
            reject(command);
    }

3、addWorker()原始碼分析:

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

            //在SHUTDOWN狀態下,如果佇列不為空,且firstTask == null,阻塞佇列裡面還有未完成的任務,則還是會繼續執行後面的操作,
            //說明了在該狀態下,能夠執行阻塞佇列裡面的任務,firstTask為空說明了是新增了一個執行阻塞佇列裡面任務的執行緒
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //獲取當前正在執行的執行緒數,判斷新增進來的任務是不是核心任務,如果是,則正在執行執行緒數要<=corePoolSize ,
                //如果不是,則正在執行執行緒數要<=maximumPoolSize。否則新建執行緒失敗。
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //執行緒數增加1,新增成功,跳出迴圈
                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 {
	        //新建一個Worker,下面說明這是個什麼東西
            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());
					//當前執行緒池的正在執行或者是SHUTDOWN狀態且firstTask為空,則要新建執行緒去執行任務。
                    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;
    }

4、Worker類是什麼?繼承自AbstractQueuedSynchronizer實現了Runnable介面(AQS相關可以參考文章原始碼分析AQS獨佔鎖、共享鎖和Condition的實現),構造方法中會初始化thread變數,AbstractQueuedSynchronizer提供了執行緒鎖的相關功能,所以Worker具有獲取和釋放鎖的能力。

 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;
            //使用factory新建執行緒
            this.thread = getThreadFactory().newThread(this);
        }
		//執行執行緒時,會呼叫該方法,runWorker()的時候會執行firstTask,可以看出Worker是對Runable的一層封裝,通過Worker的run()方法,執行實際的Runable的run()方法。
        public void run() {
            runWorker(this);
        }
    }

5、runWorker()方法,該方法開始線上程中執行我們真正的任務:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //將Worker中的Runnable拿出,並置空
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
	        //如果Worker裡面任務不為空或者佇列不為空,則執行任務,
	        //getTask()會去取阻塞佇列裡面的任務,根據阻塞佇列的策略,可能會讓核心執行緒進入等待狀態。在下面會分析
            while (task != null || (task = getTask()) != null) {
                w.lock();
                //這裡判斷執行緒池狀態,確保STOP、TIDYING和TERMINATED狀態下,如果執行緒沒有被中斷,設定執行緒被中斷標誌,
                //但並不一定會立即退出執行,因為如果執行緒中沒有sleep()、wait()、Condition、定時鎖等操作, interrupt()方法是無法中斷當前的執行緒的,
                //如果在interrupted狀態下,執行了上述的相關操作,會丟擲InterruptedException異常,直接走到finally的流程,
                //這樣會再次確保不會有核心執行緒在STOP狀態下還阻塞的情況,因為核心執行緒的存在依賴於wait()方法。
                //STOP狀態是在shutdownNow()方法中設定的,下面分析shutdownNow()和shutdown()方法的區別
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
	                //提供給開發者的一個hook,可以在任務開始之前處理一些事情
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
	                    //真正執行任務的地方
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
	                    //提供給開發者的一個hook,可以在任務開始處理完之後處理一些事情
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    //記錄完成的任務數
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
	        //無論什麼情況最終都會將執行緒從workers中移除
            processWorkerExit(w, completedAbruptly);
        }
    }

6、分析shutdown()shutdownNow()方法,為什麼shutdownNow()之後任務並不一定會停止?下面貼出主要的原始碼,兩個方法都會遍歷呼叫interrupt()方法,在該方法呼叫之後,如果有核心執行緒被阻塞,都會在上面runWorker()方法中丟擲異常,被清除(processWorkerExit()方法)。

public void shutdown() {
	//設定SHUTDOWN狀態
	advanceRunState(SHUTDOWN);
	interruptIdleWorkers();
}
private void interruptIdleWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                //w.tryLock(),嘗試獲取Worker裡面的鎖,也是和shutdownNow()方法主要區別之一,
                //在上面的runWorker()方法中也獲取了該鎖,所以如果該worker正在執行,這裡會被阻塞,t.interrupt()方法要在該worker執行完成之後才會呼叫
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
            }
        } finally {
            mainLock.unlock();
        }
    }
 
 public List<Runnable> shutdownNow() {
		//設定STOP狀態
        advanceRunState(STOP);
        interruptWorkers();
        return drainQueue();
    }
 private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
	        //這裡不會獲取worker的鎖,也就是不管該worker是不是正在執行,都強行設定中斷
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

7、核心執行緒是怎麼長期駐留的(被阻塞):getTask(),該方法負責從阻塞佇列裡取出一個任務。

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 這種狀態下,清空執行緒數
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);
			//allowCoreThreadTimeOut為true,代表允許清除核心執行緒,執行緒池中不允許存在核心執行緒
			//如果執行緒數大於了核心執行緒數,則該變數也為true
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			//線上程數大於最大執行緒數的時候要清除該執行緒,執行緒數減1,返回null,將執行緒清除
			//timed為true,且超時還沒有拿到任務且佇列為空的情況下,也會清除執行緒
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
	            //這裡會根據timed執行阻塞佇列的不同方法,在後面會分析LinkedBlockingQueue的這兩個方法,
	            //執行緒數在大於核心執行緒數的時候會執行poll(),否則執行take()方法,LinkedBlockingQueue的take()方法在佇列為空時會阻塞執行緒,
	            //keepAliveTime就是設定的執行緒存活時長,等待keepAliveTime的時間之後,如果還沒有新的任務進入佇列,則設定超時標誌timedOut
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                //超時之後還沒有新的任務可以執行,則設定timedOut標記                    
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

8、分析LinkedBlockingQueueoffer(E e)poll(long timeout, TimeUnit unit)take()方法:
①offer方法:

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        //佇列已滿直接return false,新增失敗。
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        //獲得生產者的鎖
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
	            //佇列不滿,入隊,佇列長度加1
                enqueue(node);
                //先取出之前任務數再加1
                c = count.getAndIncrement();
                //如果在放入該任務後,佇列還沒有滿,則喚醒一個等待在notFull上的執行緒,
                //因為signal()函式只會喚醒一個阻塞執行緒,因此可能有其它執行緒的put操作也被阻塞著。
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        //如果佇列任務數為0,由於剛剛加入一個任務,剛剛好佇列狀態由empty->not empty,則喚醒一個等待在notEmpty的執行緒,以便可以繼續取出任務執行
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

②poll方法:

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        //獲取消費者的鎖
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
	        //當前佇列裡如果為空了,則等待一段時間
            while (count.get() == 0) {
	            //等待時間結束之後,還沒有新的任務加入,則返回null
                if (nanos <= 0L)
                    return null;
                //notEmpty是一個條件佇列,在取任務時使用,用來執行等待超時、解除等待操作。
                nanos = notEmpty.awaitNanos(nanos);
            }
            //這個時候代表有資料了,執行出隊操作
            x = dequeue();
            //獲取當前佇列中的任務數,並將任務數減1
            c = count.getAndDecrement();
            //如果在該任務出隊後,佇列還沒有空,則喚醒一個等待在notEmpty上的執行緒,
            //因為signal()函式只會喚醒一個阻塞執行緒,因此可能有其它執行緒的poll操作也被阻塞著。
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //如果佇列任務數等於佇列容量,由於這裡取出了一個任務,剛剛好佇列狀態由full->not full狀態,如果有執行緒等待,則喚醒一個等待在notFull上的執行緒。
        if (c == capacity)
            signalNotFull();
        return x;
    }

③take方法:

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
	        //如果佇列長度為0,直接阻塞,相比於poll()沒有等待超時返回null這個操作,
	        //這也是getTask()為什麼能夠讓核心執行緒在沒有任務執行的情況下阻塞而不是被清除的原因。
            while (count.get() == 0) {
                notEmpty.await();
            }
            //如果本來佇列就不為空或者新新增進來了任務,則出隊
            x = dequeue();
            c = count.getAndDecrement();
            //同poll()操作
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //和poll()方法中的解釋一樣
        if (c == capacity)
            signalNotFull();
        return x;
    }

總結:以上分析了ThreadPoolExecutor配合BlockingQueue的使用,知其然知其所以然才能在使用中更加得心應手。在具體實際應用中,如有必要,也可以通過定義自己的阻塞佇列來實現不同的出隊入隊邏輯,通過原始碼可以瞭解其設計、增加一些新知識,讓自己成長的更快。
還是那句話,當你想了解一些原始碼設計、對一些框架一知半解、使用時總會出一些bug的時候:reading the fucking source code。

相關文章