JAVA執行緒池原理原始碼解析—為什麼啟動一個執行緒池,提交一個任務後,Main方法不會退出?

小之Evan發表於2019-03-03

起因

Hello,騷年們,大家新年快樂,頭髮有沒有少呀?今天我們來看一件有趣的事,首先來看段程式碼

public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(10);

        service.submit(() -> System.out.println("Hello "));

        System.out.println("World");
    }
複製程式碼

呵呵,執行結果誰都知道,顯而易見

JAVA執行緒池原理原始碼解析—為什麼啟動一個執行緒池,提交一個任務後,Main方法不會退出?

但是小老弟,有沒有發現這個程式一直都沒有結束呢?明明這個任務都已經跑完了呀~

結論

開始了嗎?不好意思已經結束了,嘻嘻,大過年的不賣關子,我們直接公佈答案,造成不退出的原因是這樣:

  • 你醜
  • 執行緒池的建立的時候,第一次submit操作會建立Worker執行緒(負責去拿任務處理),該執行緒裡寫了一個死迴圈,所以這個Worker執行緒不會死
  • Worker執行緒在建立的時候,被設定成了非守護執行緒thread.setDaemon(false)
  • 早在JDK1.5的時候,就規定了當所有非守護執行緒退出時,JVM才會退出,Main方法主執行緒和Worker執行緒都是非守護執行緒,所以不會死。

下面我們會就上面幾個問題,每一個問題進行原始碼分析,感興趣的看官老爺可以繼續,看看又不會掉髮(逃

原始碼分析

為什麼Worker執行緒不會死

夢開始的地方先從初始化開始

//該方法利用多臺例項化了一個ThreadPoolExecutor執行緒池,該執行緒池繼承了一個抽象類AbstractExecutorService
ExecutorService service = Executors.newFixedThreadPool(10);
//呼叫了ThreadPoolExecutor.submit方法也就是父類的AbstractExecutorService.submit,該方法內部會去呼叫execute()方法
service.submit(() -> System.out.println("Hello "));
複製程式碼

於是我們定位到ThreadPoolExecutor類的execute方法,我擷取了部分如下,注意程式碼中我打註釋的地方

public void execute(Runnable command) {
    ...
        //如果工作執行緒還沒有超過核心執行緒數
        if (workerCountOf(c) < corePoolSize) {
            //去新增工作執行緒
            if (addWorker(command, true))
                return;
        }
    ...
複製程式碼

執行緒池把每一個執行任務的工作執行緒抽象成了Worker,我們定位到內部addWorker方法

            ...
            //新建一個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());
                    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;

複製程式碼

這時候一個工作執行緒也就跑起來了,可以去執行任務了,我們定位到ThreadPoolExecutor的內部類Workerrun方法裡

//該類呼叫了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,會看這個Worker有沒有任務,如果沒有就會去取,這裡是一個死迴圈,然後我們定位到getTask()方法,看他是怎麼取任務的
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
            ...
               
    }
複製程式碼

這裡解釋了,工作執行緒其實不會死(超時時間不在本期範圍內),我們繼續定位到內部的getTask()方法,看他是怎麼取任務的

private Runnable getTask() {
            ...
            //有沒有設定核心執行緒超時時間(預設沒有)當前工作的執行緒數大於了執行緒池的核心線城市
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            ...
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    //呼叫workQueue的Take方法,WorkQueue預設是一個BlockingQueue,所以呼叫take方法會導致當前工作執行緒阻塞掉,指到拿到
                    workQueue.take();
                //如果拿到任務就返回
                if (r != null)
                    return r;
                timedOut = true;
                ...
複製程式碼

小結:

這裡想說的有兩點:

  • 工作執行緒不會死(不設定執行緒存活時間,預設情況下),會一直拿任務,所以工作執行緒會一直活著
  • 工作執行緒拿任務的時候,預設情況下,因為用的是BlockingQueuetake()拿不到任務會阻塞

Worker執行緒如何被設定成非守護執行緒

首先我們來到ThreadPoolExecutor的構造方法裡

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

構造器裡傳入了一個ThreadFactory也就是Executors.defaultThreadFactory(),用來產生工作執行緒,一步一步的點進去我們會定位到Executors內部類DefaultThreadFactorynewThread方法

public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            //關鍵程式碼是這裡,把執行緒設定成了非守護執行緒
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
複製程式碼

然後我們看ThreadPoolExector方法去new Worker()的時候

Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            //這裡的ThreadPool,就是上面提到的那個生產非守護執行緒的執行緒工廠
            this.thread = getThreadFactory().newThread(this);
        }
複製程式碼

看上面的註釋下面的內容,為什麼是非守護執行緒就真相大白了。

為什麼要等到非守護執行緒全部結束的時候,JVM才會退出?

JAVA執行緒池原理原始碼解析—為什麼啟動一個執行緒池,提交一個任務後,Main方法不會退出?

網上衝浪JdkDoc注意我標藍的部分,這跟jvm的實現有關,先知道結論,具體為什麼我們留著下期再講~

總結

跟同事在codeReview的時候,突然聊到單啟動執行緒池,Main方法會不會死明明已經都結束了呀,然後就本地跑了試了一下,跟日常的理解還是不一樣的,查了一下原因,還是蠻有趣的,日常工作中多保持好奇心,不要怕難,你會越來越強的!

你變強了,也變禿了(逃

相關文章