[第三篇]深入學習線程池之優雅的關閉線程池

通過 《深入學習線程池之線程池簡介及工作原理》、《深入學習線程池之通過ThreadPoolExecutor創建線程池及工作原理》 兩篇文章,相信大家已經了解怎么去創建一個線程池,并對線程池的工作原理有了認識,但你知道如何去關閉線程池么?直接調用shutdown()方法為什么關不掉線程池呢?shutdownNow()和shutdown()有什么區別?下面我們以ThreadPoolExecutor為例,來介紹下如何優雅的關閉線程池。在介紹線程池關閉之前,先介紹下線程中斷。

一、線程中斷

在程序中,我們不能隨便中斷一個線程,因為這是極其不安全的操作,我們無法知道這個線程正運行在什么狀態,它可能持有某把鎖,強行中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強行中斷導致數據不一致,從而混亂的問題。正因此,Java里將Thread的stop法?設置為過時,以禁止大家使用。

一個線程什么時候可以退出呢?當然只有線程自己才能知道。

所以我們這里要說的Thread的interrrupt方法,本質不是用來中斷一個線程,而是將線程設置一個中斷狀態。當我們調用線程的interrupt方法,它有兩個作用:

1、如果此線程處于阻塞狀態(比如調用了wait方法,io等待),則會立刻退出阻塞,并拋出InterruptedException異常,線程就可以通過捕獲InterruptedException來做一定的處理,然后讓線程退出。

2、如果此線程正處于運行之中,則線程不受任何影響,繼續運行,僅僅是線程的中斷標記被設置為true。所以線程要在適當的位置通過調用isInterrupted方法來查看自己是否被中斷,并做退出操作。

注:如果線程的interrupt方法先被調用,然后線程調用阻塞方法進入阻塞狀態,InterruptedException異常依舊會拋出。如果線程捕獲InterruptedException異常后,繼續調用阻塞方法, 將不再觸發InterruptedException異常。

二、線程池的兩種關閉方式

線程池提供了兩個關閉方法:shuwdown()shutdownNow() 方法。我們都知道這兩個方法的處理邏輯,如下:

shutdown()方法處理邏輯是: 線程池不再接收新提交的任務,同時等待線程池?的任務執行完畢后關閉線程池。

shutdownNow()方法處理邏輯是: 線程池不再接收新提交的任務,同時立刻關閉線程池,線程池里的任務不再執行,并返回待所有未處理的線程list列表。

但是,調用shutdown()方法后,為什么正在執?任務的線程會繼續執行完任務而不是立即停止?調用完shutdown() 或者 shutdownNow()方法后,線程池會立即關閉么?線程在什么情況下才會徹底退出?

如果不了解這些細節,在關閉線程池時就難免遇到,“線程池關閉不了”,“關閉線程池出現報錯” 等情況。下面就結合線程池源碼,分別說說這兩個線程池關閉方法的一些實現細節。

1. 線程池中執行任務的方法runWorker()

/**
     * Main worker run loop.  Repeatedly gets tasks from queue and
     * executes them, while coping with a number of issues:
     *
     * 1. We may start out with an initial task, in which case we
     * don't need to get the first one. Otherwise, as long as pool is
     * running, we get tasks from getTask. If it returns null then the
     * worker exits due to changed pool state or configuration
     * parameters.  Other exits result from exception throws in
     * external code, in which case completedAbruptly holds, which
     * usually leads processWorkerExit to replace this thread.
     *
     * 2. Before running any task, the lock is acquired to prevent
     * other pool interrupts while the task is executing, and then we
     * ensure that unless pool is stopping, this thread does not have
     * its interrupt set.
     *
     * 3. Each task run is preceded by a call to beforeExecute, which
     * might throw an exception, in which case we cause thread to die
     * (breaking loop with completedAbruptly true) without processing
     * the task.
     *
     * 4. Assuming beforeExecute completes normally, we run the task,
     * gathering any of its thrown exceptions to send to afterExecute.
     * We separately handle RuntimeException, Error (both of which the
     * specs guarantee that we trap) and arbitrary Throwables.
     * Because we cannot rethrow Throwables within Runnable.run, we
     * wrap them within Errors on the way out (to the thread's
     * UncaughtExceptionHandler).  Any thrown exception also
     * conservatively causes thread to die.
     *
     * 5. After task.run completes, we call afterExecute, which may
     * also throw an exception, which will also cause thread to
     * die. According to JLS Sec 14.20, this exception is the one that
     * will be in effect even if task.run throws.
     *
     * The net effect of the exception mechanics is that afterExecute
     * and the thread's UncaughtExceptionHandler have as accurate
     * information as we can provide about any problems encountered by
     * user code.
     *
     * @param w the worker
     */
    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) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    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 {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

正常情況下,線程池里的線程,就是在這個while循環里不停地執行。其中代碼task.run()就是在執行我們提交給線程池的任務,如下:

threadpool.execute(new Runnable() {
    @Override
    public void run() {
    // todo 具體的業務邏輯
    }
});

從runWorker()方法看得出來,如果getTask()方法返回null,會導致線程的退出。我們再來看看getTask()方法的實現:

 /**
     * Performs blocking or timed wait for a task, depending on
     * current configuration settings, or returns null if this worker
     * must exit because of any of:
     * 1. There are more than maximumPoolSize workers (due to
     *    a call to setMaximumPoolSize).
     * 2. The pool is stopped.
     * 3. The pool is shutdown and the queue is empty.
     * 4. This worker timed out waiting for a task, and timed-out
     *    workers are subject to termination (that is,
     *    {@code allowCoreThreadTimeOut || workerCount > corePoolSize})
     *    both before and after the timed wait, and if the queue is
     *    non-empty, this worker is not the last thread in the pool.
     *
     * @return task, or null if the worker must exit, in which case
     *         workerCount is decremented
     */
    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

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

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            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;
            }
        }
    }

2. shutdown()方法

當我們調用shutdown()方法時,源碼如下,我們看到,它先將線程池的狀態修改為SHUTDOWN狀態,然后調用interruptIdleWorkers()方法,來中斷空閑的線程,為什么是空閑線程呢?

在上邊runWorker方法的代碼中,我們看到獲取任務之后第一步是進行加鎖操作,即,w.lock()。而,shutdown()方法調用的interruptIdleWorkers方法,會嘗試進行w.tryLock()加鎖操作,換言之,在runWorker方法中w.lockw.unlock之間的線程將因為加鎖成功,就會導致interruptIdleWorkers方法的w.tryLock() 加鎖失敗,進而不會被調用interrupt方法,也就是說正在執行線程池里任務的線程不會被中斷。

/**
     * Initiates an orderly shutdown in which previously submitted
     * tasks are executed, but no new tasks will be accepted.
     * Invocation has no additional effect if already shut down.
     *
     * <p>This method does not wait for previously submitted tasks to
     * complete execution.  Use {@link #awaitTermination awaitTermination}
     * to do that.
     *
     * @throws SecurityException {@inheritDoc}
     */
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN); // ① 將線程池狀態置為SHUTDOWN
            interruptIdleWorkers();    // ② 停用線程池中的線程
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
/**
     * Interrupts threads that might be waiting for tasks (as
     * indicated by not being locked) so they can check for
     * termination or configuration changes. Ignores
     * SecurityExceptions (in which case some threads may remain
     * uninterrupted).
     *
     * @param onlyOne If true, interrupt at most one worker. This is
     * called only from tryTerminate when termination is otherwise
     * enabled but there are still other workers.  In this case, at
     * most one waiting worker is interrupted to propagate shutdown
     * signals in case all threads are currently waiting.
     * Interrupting any arbitrary thread ensures that newly arriving
     * workers since shutdown began will also eventually exit.
     * To guarantee eventual termination, it suffices to always
     * interrupt only one idle worker, but shutdown() interrupts all
     * idle workers so that redundant workers exit promptly, not
     * waiting for a straggler task to finish.
     */
    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {// 線程沒有被中斷且worker獲取到鎖的時候才處理
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

    /**
     * Common form of interruptIdleWorkers, to avoid having to
     * remember what the boolean argument means.
     */
    private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

3. shutdownNow()方法

當我們調用shutdownNow()方法時,源碼如下,我們看到,它先將線程池的狀態修改為STOP狀態,然后調用interruptWorkers()方法,遍歷中斷線程,最后返回未執行的任務的線程list。

在runWorker方法中,代碼task.run()就是在執行我們提交給線程池的任務,當我們調用shutdownNow方法時,task.run()里面正處于IO阻塞,即,我們提交任務的邏輯,涉及到IO阻塞,則會導致報錯,如果task.run()里正在正常執行,則不受影響,繼續執行完這個任務。

/**
     * Attempts to stop all actively executing tasks, halts the
     * processing of waiting tasks, and returns a list of the tasks
     * that were awaiting execution. These tasks are drained (removed)
     * from the task queue upon return from this method.
     *
     * <p>This method does not wait for actively executing tasks to
     * terminate.  Use {@link #awaitTermination awaitTermination} to
     * do that.
     *
     * <p>There are no guarantees beyond best-effort attempts to stop
     * processing actively executing tasks.  This implementation
     * cancels tasks via {@link Thread#interrupt}, so any task that
     * fails to respond to interrupts may never terminate.
     *
     * @throws SecurityException {@inheritDoc}
     */
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
/**
     * Interrupts all threads, even if active. Ignores SecurityExceptions
     * (in which case some threads may remain uninterrupted).
     */
    private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

總結

一、當我們調用線程池的shutdownNow方法時,會將線程池狀態修改為STOP,當執行runWorker方法中while (task != null || (task = getTask()) != null)時,在getTask方法中,由于STOP狀態值是大于SHUTDOWN狀態,STOP也大于等于STOP,所以不管任務隊列是否為空,都會進入if語句,即,

 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
  }

從而返回null,導致 (task = getTask()) != null條件不成立,進而執行線程退出。

二、當我們調用線程池的shuwdown方法時,會將線程池狀態修改為SHUTDOWN,當執行runWorker方法中while (task != null || (task = getTask()) != null)時,在getTask方法中,SHUTDOWN大于等于SHUTDOWN成立沒問題,但是SHUTDOWN不大于等于STOP狀態,所以只有隊列為空,getTask方法才會返回null,導致線程退出。如果線程正在執行線程池里的任務,即便任務處于阻塞狀態,線程也不會被中斷,而是繼續執行。如果線程池阻塞等待從隊列里讀取任務,則會被喚醒,但是會繼續判斷隊列是否為空,若不為空,則會繼續從隊列里讀取任務,若為空則線程退出。

優雅的關閉線程池

使用shutdownNow?法,可能會引起報錯,使用shutdown方法可能會導致線程關閉不了。

所以當我們使用shutdownNow?法關閉線程池時,一定要對任務里進行異常捕獲。即,在我們提交的任務里有try{}catch{}處理

當我們使用shuwdown方法關閉線程池時,一定要確保任務里不會有永久阻塞等待的邏輯,否則線程池就關閉不了。

最后,一定要記得shutdownNow和shuwdown調用完,線程池并不是立刻就關閉了,要想等待線程池關閉,還需調用awaitTermination方法來阻塞等待。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容