1. 程式人生 > 遊戲資訊 >Keria:比賽時肌肉絞痛,身體很不舒服!感覺像在地獄!

Keria:比賽時肌肉絞痛,身體很不舒服!感覺像在地獄!

Java 執行緒池會自動關閉嗎|轉

  首先我們需要了解執行緒池在什麼情況下會自動關閉。ThreadPoolExecutor 類(這是我們最常用的執行緒池實現類)的原始碼註釋中有這麼一句話:

A pool that is no longer referenced in a program and has no remaining threads will be shutdown automatically.

沒有引用指向且沒有剩餘執行緒的執行緒池將會自動關閉。

那麼什麼情況下執行緒池中會沒有剩餘執行緒呢?先來看一下 ThreadPoolExecutor 引數最全的構造方法:

/**
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 *        核心執行緒數:即使是空閒狀態也可以線上程池存活的執行緒數量,除非        
 *        allowCoreThreadTimeOut 設定為 true。
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 *       存活時間:對於超出核心執行緒數的執行緒,空閒時間一旦達到存活時間,就會被銷燬。
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) { ... ... }

  這裡我們只關心與執行緒存活狀態最緊密相關的兩個引數,也就是corePoolSizekeepAliveTime,上述程式碼塊也包含了這兩個引數的原始碼註釋和中文翻譯。keepAliveTime引數指定了非核心執行緒的存活時間,非核心執行緒的空閒時間一旦達到這個值,就會被銷燬,而核心執行緒則會繼續存活,只要有執行緒存活,執行緒池也就不會自動關閉。聰明的你一定會想到,如果把corePoolSize設定為0,再給keepAliveTime指定一個值的話,那麼執行緒池在空閒一段時間之後,不就可以自動關閉了嗎?沒錯,這就是執行緒池自動關閉的第一種情況。

1. 核心執行緒數為 0 並指定執行緒存活時間

1.1. 手動建立執行緒池

程式碼示例:

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 重點關注 corePoolSize 和 keepAliveTime,其他引數不重要
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5,
                30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
               // 簡單地列印當前執行緒名稱
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

控制檯輸出結果

# 執行緒列印開始
... ...
pool-1-thread-2                    
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-1
# 列印結束,程式等待30s後正常退出
Process finished with exit code 0   # 小知識:exit code 0 說明程式是正常退出,非強行中斷或異常退出

  通過以上程式碼和執行結果可以得知,在corePoolSize為0且keepAliveTime設定為 60s 的情況下,如果任務執行完畢又沒有新的任務到來,執行緒池裡的執行緒都將消亡,而且沒有核心執行緒阻止執行緒池關閉,因此執行緒池也將隨之自動關閉。

  而如果將corePoolSize設定為大於0的數字,再執行以上程式碼,那麼執行緒池將一直處於等待狀態而不能關閉,因為核心執行緒不受keepAliveTime控制,所以會一直存活,程式也將一直不能結束。執行效果如下 (corePoolSize設定為5,其他引數不變):

# 執行緒列印開始
... ...
pool-1-thread-5
pool-1-thread-1
pool-1-thread-3
pool-1-thread-4
pool-1-thread-2
# 列印結束,但程式無法結束

2.2 Executors.newCachedThrteadPool() 建立執行緒池

  Executors 是 JDK 自帶的執行緒池框架類,包含多個建立不同型別執行緒池的方法,而其中的newCachedThrteadPool()方法也將核心執行緒數設定為了0並指定了執行緒存活時間,所以也可以自動關閉。其原始碼如下:

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

  如果用這個執行緒池執行上面的程式碼,程式也會自動退出,效果如下

# 執行緒列印開始
... ...
pool-1-thread-7
pool-1-thread-5
pool-1-thread-4
pool-1-thread-1
pool-1-thread-9
# 列印結束,程式等待60s後退出
Process finished with exit code 0

2. 通過 allowCoreThreadTimeOut 控制核心執行緒存活時間

  通過將核心執行緒數設定為0雖然可以實現執行緒池的自動關閉,但也存在一些弊端,新到來的任務若發現沒有活躍執行緒,則會優先被放入任務佇列,然後等待被處理,這顯然會影響程式的執行效率。那你可能要問了,有沒有其他的方法來自己實現可自動關閉的執行緒池呢?答案是肯定的,從 JDK 1.6 開始,ThreadPoolExecutor 類新增了一個allowCoreThreadTimeOut欄位:

/**
 * If false (default), core threads stay alive even when idle.
 * If true, core threads use keepAliveTime to time out waiting
 * for work.
 * 預設為false,核心執行緒處於空閒狀態也可一直存活
 * 如果設定為true,核心執行緒的存活狀態將受keepAliveTime控制,超時將被銷燬
 */
private volatile boolean allowCoreThreadTimeOut;

  這個欄位值預設為false,可使用allowCoreThreadTimeOut()方法對其進行設定,如果設定為 true,那麼核心執行緒數也將受keepAliveTime控制,此方法原始碼如下:

public void allowCoreThreadTimeOut(boolean value) {
    // 核心執行緒存活時間必須大於0,一旦開啟,keepAliveTime 也必須大於0
    if (value && keepAliveTime <= 0)
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    // 將 allowCoreThreadTimeOut 值設為傳入的引數值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 開啟後,清理所有的超時空閒執行緒,包括核心執行緒
        if (value)
            interruptIdleWorkers();
    }
}

  既然如此,接下來我們就藉助這個方法實現一個可自動關閉且核心執行緒數不為0的執行緒池,這裡直接在第一個程式的基礎上進行改進:

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 這裡把corePoolSize設為5,keepAliveTime保持不變
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
        // 允許核心執行緒超時銷燬
        executor.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

  執行結果:

# 執行緒列印開始
... ...
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
# 列印結束,程式等待30s後退出
Process finished with exit code 0

  可以看到,程式在列印結束後等待了30s,然後自行退出,說明執行緒池已自動關閉,也就是allowCoreThreadTimeOut()方法發揮了作用。這樣,我們就實現了可自動關閉且核心執行緒數不為0的執行緒池。

3. 超詳細的執行緒池執行流程圖

  讓我們再來梳理一下更完整的執行緒池執行流程:


詳細的執行緒池執行流程圖

4. 小結

  以上就是執行緒池可以自動關閉的兩種情況,而且梳理了詳細的執行緒池執行流程,相信你看完本文一定會有所收穫。不過話又說回來,可自動關閉的執行緒池的實際應用場景並不多,更多時候需要我們手動關閉。在執行完任務後呼叫ExecutorService的shutdown()方法,具體測試用例請參考《Java 自定義執行緒池的執行緒工廠》一文中優雅的自定義執行緒工廠這一節。

Reference

  首先我們需要了解執行緒池在什麼情況下會自動關閉。ThreadPoolExecutor 類(這是我們最常用的執行緒池實現類)的原始碼註釋中有這麼一句話:

A pool that is no longer referenced in a program and has no remaining threads will be shutdown automatically.

沒有引用指向且沒有剩餘執行緒的執行緒池將會自動關閉。

那麼什麼情況下執行緒池中會沒有剩餘執行緒呢?先來看一下 ThreadPoolExecutor 引數最全的構造方法:

/**
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 *        核心執行緒數:即使是空閒狀態也可以線上程池存活的執行緒數量,除非        
 *        allowCoreThreadTimeOut 設定為 true。
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 *       存活時間:對於超出核心執行緒數的執行緒,空閒時間一旦達到存活時間,就會被銷燬。
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) { ... ... }

  這裡我們只關心與執行緒存活狀態最緊密相關的兩個引數,也就是corePoolSizekeepAliveTime,上述程式碼塊也包含了這兩個引數的原始碼註釋和中文翻譯。keepAliveTime引數指定了非核心執行緒的存活時間,非核心執行緒的空閒時間一旦達到這個值,就會被銷燬,而核心執行緒則會繼續存活,只要有執行緒存活,執行緒池也就不會自動關閉。聰明的你一定會想到,如果把corePoolSize設定為0,再給keepAliveTime指定一個值的話,那麼執行緒池在空閒一段時間之後,不就可以自動關閉了嗎?沒錯,這就是執行緒池自動關閉的第一種情況。

1. 核心執行緒數為 0 並指定執行緒存活時間

1.1. 手動建立執行緒池

程式碼示例:

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 重點關注 corePoolSize 和 keepAliveTime,其他引數不重要
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5,
                30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
               // 簡單地列印當前執行緒名稱
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

控制檯輸出結果

# 執行緒列印開始
... ...
pool-1-thread-2                    
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-1
# 列印結束,程式等待30s後正常退出
Process finished with exit code 0   # 小知識:exit code 0 說明程式是正常退出,非強行中斷或異常退出

  通過以上程式碼和執行結果可以得知,在corePoolSize為0且keepAliveTime設定為 60s 的情況下,如果任務執行完畢又沒有新的任務到來,執行緒池裡的執行緒都將消亡,而且沒有核心執行緒阻止執行緒池關閉,因此執行緒池也將隨之自動關閉。

  而如果將corePoolSize設定為大於0的數字,再執行以上程式碼,那麼執行緒池將一直處於等待狀態而不能關閉,因為核心執行緒不受keepAliveTime控制,所以會一直存活,程式也將一直不能結束。執行效果如下 (corePoolSize設定為5,其他引數不變):

# 執行緒列印開始
... ...
pool-1-thread-5
pool-1-thread-1
pool-1-thread-3
pool-1-thread-4
pool-1-thread-2
# 列印結束,但程式無法結束

2.2 Executors.newCachedThrteadPool() 建立執行緒池

  Executors 是 JDK 自帶的執行緒池框架類,包含多個建立不同型別執行緒池的方法,而其中的newCachedThrteadPool()方法也將核心執行緒數設定為了0並指定了執行緒存活時間,所以也可以自動關閉。其原始碼如下:

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

  如果用這個執行緒池執行上面的程式碼,程式也會自動退出,效果如下

# 執行緒列印開始
... ...
pool-1-thread-7
pool-1-thread-5
pool-1-thread-4
pool-1-thread-1
pool-1-thread-9
# 列印結束,程式等待60s後退出
Process finished with exit code 0

2. 通過 allowCoreThreadTimeOut 控制核心執行緒存活時間

  通過將核心執行緒數設定為0雖然可以實現執行緒池的自動關閉,但也存在一些弊端,新到來的任務若發現沒有活躍執行緒,則會優先被放入任務佇列,然後等待被處理,這顯然會影響程式的執行效率。那你可能要問了,有沒有其他的方法來自己實現可自動關閉的執行緒池呢?答案是肯定的,從 JDK 1.6 開始,ThreadPoolExecutor 類新增了一個allowCoreThreadTimeOut欄位:

/**
 * If false (default), core threads stay alive even when idle.
 * If true, core threads use keepAliveTime to time out waiting
 * for work.
 * 預設為false,核心執行緒處於空閒狀態也可一直存活
 * 如果設定為true,核心執行緒的存活狀態將受keepAliveTime控制,超時將被銷燬
 */
private volatile boolean allowCoreThreadTimeOut;

  這個欄位值預設為false,可使用allowCoreThreadTimeOut()方法對其進行設定,如果設定為 true,那麼核心執行緒數也將受keepAliveTime控制,此方法原始碼如下:

public void allowCoreThreadTimeOut(boolean value) {
    // 核心執行緒存活時間必須大於0,一旦開啟,keepAliveTime 也必須大於0
    if (value && keepAliveTime <= 0)
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    // 將 allowCoreThreadTimeOut 值設為傳入的引數值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 開啟後,清理所有的超時空閒執行緒,包括核心執行緒
        if (value)
            interruptIdleWorkers();
    }
}

  既然如此,接下來我們就藉助這個方法實現一個可自動關閉且核心執行緒數不為0的執行緒池,這裡直接在第一個程式的基礎上進行改進:

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 這裡把corePoolSize設為5,keepAliveTime保持不變
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
        // 允許核心執行緒超時銷燬
        executor.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

  執行結果:

# 執行緒列印開始
... ...
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
# 列印結束,程式等待30s後退出
Process finished with exit code 0

  可以看到,程式在列印結束後等待了30s,然後自行退出,說明執行緒池已自動關閉,也就是allowCoreThreadTimeOut()方法發揮了作用。這樣,我們就實現了可自動關閉且核心執行緒數不為0的執行緒池。

3. 超詳細的執行緒池執行流程圖

  讓我們再來梳理一下更完整的執行緒池執行流程:


詳細的執行緒池執行流程圖

4. 小結

  以上就是執行緒池可以自動關閉的兩種情況,而且梳理了詳細的執行緒池執行流程,相信你看完本文一定會有所收穫。不過話又說回來,可自動關閉的執行緒池的實際應用場景並不多,更多時候需要我們手動關閉。在執行完任務後呼叫ExecutorService的shutdown()方法,具體測試用例請參考《Java 自定義執行緒池的執行緒工廠》一文中優雅的自定義執行緒工廠這一節。

Reference