通過原始碼理解 Java 執行緒池的核心引數

candyleer發表於2019-03-17

背景

作為一名 Java 開發者,對執行緒池絕對不陌生,無論是平時工作,還是面試都會,執行緒池都是必會的知識點.而且不能不能之知其表面,理解不透徹,很容易在實戰中出現 OOM,也可能在面試中被問趴?.

引數含義

其實,如果研究過執行緒池的話,其實並不難,他的引數並不多,java.util.concurrent.ThreadPoolExecutor中的引數列舉出來就是這些.

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
複製程式碼
  • corePoolSize:執行緒池中的核心執行緒數,即使沒有任務執行的時候,他們也是存在的.(不考慮配置了引數:allowCoreThreadTimeOut,allowCoreThreadTimeOut通過字面意思也能知道,就是是否允許核心執行緒超時,一般情況下不需要設定,本文不考慮)
  • maximumPoolSize:執行緒池中的允許存在的最大執行緒數.
  • keepAliveTime: 當執行緒池中的執行緒超過核心執行緒數的時候,這部分多餘的空閒執行緒等待執行新任務的超時時間.例如:核心執行緒數為1 ,最大執行緒數為5,當前執行執行緒為4,keepAliveTime為60s,那麼4-1=3個執行緒在空閒狀態下等待60s 後還沒有新任務到來,就會被銷燬了.
  • unit :keepAliveTime 的時間單位.
  • workQueue: 執行緒佇列,如果當前時間核心執行緒都在執行,又來了一個新任務,那麼這個新任務就會被放進這個執行緒佇列中,等待執行.
  • threadFactory: 執行緒池建立執行緒的工廠類.
  • handler: 如果執行緒佇列滿了同事執行執行緒數也達到了maximumPoolSize,如果此時再來新的執行緒,將執行什麼 handler 來處理這個執行緒. handler的預設提供的型別有:
    • AbortPolicy: 丟擲RejectedExecutionException異常
    • DiscardPolicy: 什麼都不做.
    • DiscardOldestPolicy: 將執行緒佇列中的最老的任務拋棄掉,換區一個空間執行當前的任務.
    • CallerRunsPolicy: 使用當前的執行緒(比如 main)來執行這個執行緒.

通過原始碼理解 Java 執行緒池的核心引數

執行流程

我們知道了引數的含義,那麼這些引數在執行過程中到底是怎麼執行的呢,我們先用文字分幾種情況來描述一下.

在說之前,先來看一個例子.

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new BasicThreadFactory.Builder().namingPattern("name-%d").build());
threadPoolExecutor.execute(new Runnable() {
           @Override
           public void run() {
               System.out.println("test");
           }
       });
複製程式碼

不得不說,在很長一段時間內,我都有一個疑問或者說誤區,明明是執行緒池,為什麼每次都需要我 new 一個 執行緒(錯誤) 呢? 因為我們開始學執行緒的時候先學 new Thread(),後來又學了new Runnable(),慢慢的就把這兩個混為一罈了,其實 new Runnable()並沒有新起一個執行緒,只是新建了一個可執行的任務,就是一個普通的物件而已,哈哈,這應該是一個很傻的錯誤認知. 回到上面說的具體含義.

  1. 如果新加入一個執行的任務,當前執行的執行緒小於corePoolSize,這時候會線上程池中新建一個執行緒用於執行這個新的任務.
  2. 如果新加入一個執行的任務,當前執行的執行緒大於等於corePoolSize,這個時候就需要將這個新的任務加入到執行緒佇列workQueue中,一旦執行緒中的執行緒執行完成了一個任務,就會馬上從佇列中去一個任務來執行.
  3. 接2,如果佇列也滿了,怎麼辦呢? 如果maximumPoolSize大於corePoolSize,就會新建執行緒來處理這個新的任務,知道總執行執行緒數達到maximumPoolSize.
  4. 如果總執行執行緒數達到了maximumPoolSize,還來了新的任務怎麼辦呢?就需要執行上面所說的拒絕策略了handler了,按照配置的策略進行處理,預設不配置的情況下,使用的是AbortPolicy.
 private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
複製程式碼

原始碼驗證

怎麼判斷上面說的流程是正確的呢?我們可以跟進原始碼來仔細檢視一下上面的流程,其實執行緒池執行的程式碼比較簡單,一看變動,看了原始碼,掌握得應該會更加深刻一些.

首先來看看execute()方法

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // ctl是一個原子的控制位,可以表示執行緒池的狀態和執行的執行緒數;
        int c = ctl.get();
        // 1. 如果執行執行緒數小於核心執行緒數
        if (workerCountOf(c) < corePoolSize) {
            //直接新建 worker(執行緒)執行.
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2. 如果上面的addWorker 失敗了,就需要加入執行緒佇列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 加入後,檢查狀態;
            if (! isRunning(recheck) && remove(command))
                //檢查執行狀態不通過,移除任務,執行拒絕策略
                reject(command);
            // 如果當前的執行執行緒為0
            else if (workerCountOf(recheck) == 0)
                //就是用核心執行緒執行剛剛新增到佇列的執行緒
                addWorker(null, false);
        }
        // 3. 如果佇列也滿了,就新建執行緒繼續處理
        else if (!addWorker(command, false))
            // 4. 如果不允許新建了,就執行拒絕策略
            reject(command);
    }
複製程式碼

按照一個正常流程來說,我們只考慮一個理想的環境.我們可以分為上面的4步,正好和上面的文字描述對應.

可能愛思考的同學發現,第2步,加入佇列後,什麼時候執行這個新加入的呢,難道有一個定時任務嗎?並不是.我們可以看看這個addWorker()方法.

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        //第一層迴圈
        for (;;) {
            int c = ctl.get();
            //獲取當前執行緒池的狀態;
            int rs = runStateOf(c);
            ...
            //第二層迴圈
            for (;;) {
                //獲取執行緒池的執行執行緒個數
                int wc = workerCountOf(c);
                // 大於了最大允許的執行緒個數,當然要返回 false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //通過了檢查,就把 正在執行的執行緒數加1
                if (compareAndIncrementWorkerCount(c))
                    //跳出第一層迴圈
                    break retry;
                c = ctl.get();  // Re-read ctl
                //加1 失敗,可能有多執行緒衝突,檢查一下最新的狀態,繼續重試;
                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 {
            //新建一個執行緒包裝我們的 Runnable
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                ...
                //加入 hashSet 中管理存在於執行緒池中執行緒
                workers.add(w);
                workerAdded = true;
                if (workerAdded) {
                   // 啟動 worker,worker就是執行緒中真正執行的執行緒,包裝了我們提供的 Runnable
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
複製程式碼

上面的addWorker()方法中,就是靠t.start()來啟動執行緒的. Worker這個類存在於java.util.concurrent.ThreadPoolExecutor.Worker,定義如下 只保留了相對重要的程式碼.

 private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
             Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }
        
        final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                   ....
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    ...
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
        }
複製程式碼

所以當t.start()的時候,實際上,新建了一個執行緒,執行了runWorker(this);方法: 這個方法裡面有一個while迴圈,getTask()是從佇列中獲取一個任務.所以說這裡可以解答上面放到佇列裡面的任務什麼時候執行了,等到任意一個核心執行緒空閒出來時候,他就會迴圈去取佇列中的任務執行.每個核心執行緒和新起來的執行緒都是同步來執行你傳進來的Runnablerun方法.

整個流程應該就比較清楚了.

上面說了這麼多,核心引數都說的差不多了,那麼keepAliveTime 這個引數在原始碼怎麼來用的呢?

上面說到一個getTask()方法從佇列中取一個任務,看一下這個方法的程式碼(省略非主要的).

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

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

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
複製程式碼

主要就是用於取任務這裡,poll()不會阻塞,take()是阻塞的,所以當使用poll取資料的時候,到達設定的超時,就會繼續往下執行,如果超過設定時間還是沒有任務進來,就會將timedOut設定為 true,返回 null. 這個timedOut會控制上面的 if 判斷,最終控制compareAndDecrementWorkerCount()方法,就是講執行的執行緒數減1個,那麼下次如果又滿了,就會新建一個,所以這個 Alive 就失效了.

總結

總體來說,通過原始碼來看問題能比較權威的解答一些問題.有時候原始碼似乎也沒有那麼高深?

相關文章