程序員必需要會的線程池,附10道面試題

爲何要用線程池呢?

下面是一段建立線程並運行的代碼:java

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        System.out.println("run thread->" + Thread.currentThread().getName());
        userService.updateUser(....);
    }).start();
}

咱們想使用這種方式去作異步,或者提升性能,而後將某些耗時操做放入一個新線程去運行。面試

這種思路是沒問題的,可是這段代碼是存在問題的,有哪些問題呢?下面咱們就來看看有哪些問題;數據庫

  • 建立銷燬線程資源消耗;咱們使用線程的目的本是出於效率考慮,能夠爲了建立這些線程卻消耗了額外的時間,資源,對於線程的銷燬一樣須要系統資源。緩存

  • cpu資源有限,上述代碼建立線程過多,形成有的任務不能即時完成,響應時間過長。網絡

  • 線程沒法管理,無節制地建立線程對於有限的資源來講彷佛成了「得不償失」的一種做用。併發

既然咱們上面使用手動建立線程會存在問題,那有解決方法嗎?異步

答案:有的,使用線程池。ide

線程池介紹

線程池(Thread Pool):把一個或多個線程經過統一的方式進行調度和重複使用的技術,避免了由於線程過多而帶來使用上的開銷。性能

線程池有什麼優勢?

  • 下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。測試

  • 提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。

  • 提升線程的可管理性。

線程池使用

在JDK中rt.jar包下JUC(java.util.concurrent)建立線程池有兩種方式:ThreadPoolExecutor 和 Executors,其中 Executors又能夠建立 6 種不一樣的線程池類型。

ThreadPoolExecutor 的使用

線程池使用代碼以下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));

    public static void main(String[] args) {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("田先生您好");
            }
        });
    }
}

以上程序執行結果以下:

田先生您好

核心參數說明

ThreadPoolExecutor的構造方法有如下四個:

img

能夠看到最後那個構造方法有 7 個構造參數,其實前面的三個構造方法只是對最後那個方法進行包裝,而且前面三個構造方法最終都是調用最後那個構造方法,因此咱們這裏就來聊聊最後那個構造方法。

參數解釋

corePoolSize

線程池中的核心線程數,默認狀況下核心線程一直存活在線程池中,若是將 ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設爲 true,若是線程池一直閒置並超過了 keepAliveTime 所指定的時間,核心線程就會被終止。

maximumPoolSize

最大線程數,當線程不夠時可以建立的最大線程數。

keepAliveTime

線程池的閒置超時時間,默認狀況下對非核心線程生效,若是閒置時間超過這個時間,非核心線程就會被回收。若是 ThreadPoolExecutor 的 allowCoreThreadTimeOut 設爲 true 的時候,核心線程若是超過閒置時長也會被回收。

unit

配合 keepAliveTime 使用,用來標識 keepAliveTime 的時間單位。

workQueue

線程池中的任務隊列,使用 execute() 或 submit() 方法提交的任務都會存儲在此隊列中。

threadFactory

爲線程池提供建立新線程的線程工廠。

rejectedExecutionHandler

線程池任務隊列超過最大值以後的拒絕策略,RejectedExecutionHandler 是一個接口,裏面只有一個 rejectedExecution 方法,可在此方法內添加任務超出最大值的事件處理。ThreadPoolExecutor 也提供了 4 種默認的拒絕策略:

  • DiscardPolicy():丟棄掉該任務,不進行處理。

  • DiscardOldestPolicy():丟棄隊列裏最近的一個任務,並執行當前任務。

  • AbortPolicy():直接拋出 RejectedExecutionException 異常(默認)。

  • CallerRunsPolicy():既不拋棄任務也不拋出異常,直接使用主線程來執行此任務。

包含全部參數的使用案例:

public class ThreadPoolExecutorTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
                new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        threadPool.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
class MyThreadFactory implements ThreadFactory {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        String threadName = "MyThread" + count.addAndGet(1);
        t.setName(threadName);
        return t;
    }
}

運行輸出:

main
MyThread1
main
MyThread1
MyThread1
....

這裏僅僅是爲了演示全部參數自定義,並無其餘用途。

execute() 和 submit()的使用

execute() 和 submit() 都是用來執行線程池的,區別在於 submit() 方法能夠接收線程池執行的返回值。

下面分別來看兩個方法的具體使用和區別:

// 建立線程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 使用
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("老田您好");
    }
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("田先生您好");
        return "返回值";
    }
});
System.out.println(future.get());

以上程序執行結果以下:

老田您好
田先生您好
返回值

Executors

Executors 執行器建立線程池不少基本上都是在 ThreadPoolExecutor 構造方法上進行簡單的封裝,特殊場景根據須要自行建立。能夠把Executors理解成一個工廠類 。Executors能夠建立 6 種不一樣的線程池類型。

下面對這六個方法進行簡要的說明:

newFixedThreadPool

建立一個數量固定的線程池,超出的任務會在隊列中等待空閒的線程,可用於控制程序的最大併發數。

newCacheThreadPool

短期內處理大量工做的線程池,會根據任務數量產生對應的線程,並試圖緩存線程以便重複使用,若是限制 60 秒沒被使用,則會被移除緩存。若是現有線程沒有可用的,則建立一個新線程並添加到池中,若是有被使用完可是還沒銷燬的線程,就複用該線程。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。所以,長時間保持空閒的線程池不會使用任何資源。

newScheduledThreadPool

建立一個數量固定的線程池,支持執行定時性或週期性任務。

newWorkStealingPool

Java 8 新增建立線程池的方法,建立時若是不設置任何參數,則以當前機器CPU 處理器數做爲線程個數,此線程池會並行處理任務,不能保證執行順序。

newSingleThreadExecutor

建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。

newSingleThreadScheduledExecutor

此線程池就是單線程的 newScheduledThreadPool。

線程池如何關閉?

線程池關閉,可使用 shutdown() 或 shutdownNow() 方法,它們的區別是:

  • shutdown():不會當即終止線程池,而是要等全部任務隊列中的任務都執行完後纔會終止。執行完 shutdown 方法以後,線程池就不會再接受新任務了。

  • shutdownNow():執行該方法,線程池的狀態馬上變成 STOP 狀態,並試圖中止全部正在執行的線程,再也不處理還在池隊列中等待的任務,執行此方法會返回未執行的任務。

下面用代碼來模擬 shutdown() 以後,給線程池添加任務,代碼以下:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolExecutorAllArgsTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException {
       //建立線程池
       ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
               10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
               new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
       threadPoolExecutor.allowCoreThreadTimeOut(true);
       //提交任務
       threadPoolExecutor.execute(() -> {
           for (int i = 0; i < 3; i++) {
               System.out.println("提交任務" + i);
               try {
                   Thread.sleep(3000);
               } catch (InterruptedException e) {
                   System.out.println(e.getMessage());
               }
           }
       });
       threadPoolExecutor.shutdown();
       //再次說起任務
       threadPoolExecutor.execute(() -> {
           System.out.println("我想再次說起任務");
       });
   }
}

以上程序執行結果以下:

提交任務0
提交任務1
提交任務2

能夠看出,shutdown() 以後就不會再接受新的任務了,不過以前的任務會被執行完成。

面試題

面試題1:ThreadPoolExecutor 有哪些經常使用的方法?

ThreadPoolExecutor有以下經常使用方法:

  • submit()/execute():執行線程池

  • shutdown()/shutdownNow():終止線程池

  • isShutdown():判斷線程是否終止

  • getActiveCount():正在運行的線程數

  • getCorePoolSize():獲取核心線程數

  • getMaximumPoolSize():獲取最大線程數

  • getQueue():獲取線程池中的任務隊列

  • allowCoreThreadTimeOut(boolean):設置空閒時是否回收核心線程

這些方法能夠用來終止線程池、線程池監控等。

面試題2:說說submit(和 execute兩個方法有什麼區別?

submit() 和 execute() 都是用來執行線程池的,只不過使用 execute() 執行線程池不能有返回方法,而使用 submit() 可使用 Future 接收線程池執行的返回值。

說說線程池建立須要的那幾個核心參數的含義

ThreadPoolExecutor 最多包含如下七個參數:

  • corePoolSize:線程池中的核心線程數

  • maximumPoolSize:線程池中最大線程數

  • keepAliveTime:閒置超時時間

  • unit:keepAliveTime 超時時間的單位(時/分/秒等)

  • workQueue:線程池中的任務隊列

  • threadFactory:爲線程池提供建立新線程的線程工廠

  • rejectedExecutionHandler:線程池任務隊列超過最大值以後的拒絕策略

面試題3:shutdownNow() 和 shutdown() 兩個方法有什麼區別?

shutdownNow() 和 shutdown() 都是用來終止線程池的,它們的區別是,使用 shutdown() 程序不會報錯,也不會當即終止線程,它會等待線程池中的緩存任務執行完以後再退出,執行了 shutdown() 以後就不能給線程池添加新任務了;shutdownNow() 會試圖立馬中止任務,若是線程池中還有緩存任務正在執行,則會拋出 java.lang.InterruptedException: sleep interrupted 異常。

面試題6:瞭解過線程池的工做原理嗎?

img

當線程池中有任務須要執行時,線程池會判斷若是線程數量沒有超過核心數量就會新建線程池進行任務執行,若是線程池中的線程數量已經超過核心線程數,這時候任務就會被放入任務隊列中排隊等待執行;若是任務隊列超過最大隊列數,而且線程池沒有達到最大線程數,就會新建線程來執行任務;若是超過了最大線程數,就會執行拒絕執行策略。

面試題5:線程池中核心線程數量大小怎麼設置?

「CPU密集型任務」:好比像加解密,壓縮、計算等一系列須要大量耗費 CPU 資源的任務,大部分場景下都是純 CPU 計算。儘可能使用較小的線程池,通常爲CPU核心數+1。由於CPU密集型任務使得CPU使用率很高,若開過多的線程數,會形成CPU過分切換。

「IO密集型任務」:好比像 MySQL 數據庫、文件的讀寫、網絡通訊等任務,這類任務不會特別消耗 CPU 資源,可是 IO 操做比較耗時,會佔用比較多時間。可使用稍大的線程池,通常爲2*CPU核心數。IO密集型任務CPU使用率並不高,所以可讓CPU在等待IO的時候有其餘線程去處理別的任務,充分利用CPU時間。

另外:線程的平均工做時間所佔比例越高,就須要越少的線程;線程的平均等待時間所佔比例越高,就須要越多的線程;

以上只是理論值,實際項目中建議在本地或者測試環境進行屢次調優,找到相對理想的值大小。

面試題7:線程池爲何須要使用(阻塞)隊列?

主要有三點:

  • 由於線程如果無限制的建立,可能會致使內存佔用過多而產生OOM,而且會形成cpu過分切換。

  • 建立線程池的消耗較高。

面試題8:線程池爲何要使用阻塞隊列而不使用非阻塞隊列?

阻塞隊列能夠保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。

當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。

使得在線程不至於一直佔用cpu資源。

(線程執行完任務後經過循環再次從任務隊列中取出任務進行執行,代碼片斷以下

 while (task != null || (task = getTask()) != null) {})。

不用阻塞隊列也是能夠的,不過實現起來比較麻煩而已,有好用的爲啥不用呢?

面試題9:瞭解線程池狀態嗎?

經過獲取線程池狀態,能夠判斷線程池是不是運行狀態、能否添加新的任務以及優雅地關閉線程池等。

img

  • RUNNING:線程池的初始化狀態,能夠添加待執行的任務。

  • SHUTDOWN:線程池處於待關閉狀態,不接收新任務僅處理已經接收的任務。

  • STOP:線程池當即關閉,不接收新的任務,放棄緩存隊列中的任務而且中斷正在處理的任務。

  • TIDYING:線程池自主整理狀態,調用 terminated() 方法進行線程池整理。

  • TERMINATED:線程池終止狀態。

面試題10:知道線程池中線程複用原理嗎?

線程池將線程和任務進行解耦,線程是線程,任務是任務,擺脫了以前經過 Thread 建立線程時的一個線程必須對應一個任務的限制。

在線程池中,同一個線程能夠從阻塞隊列中不斷獲取新任務來執行,其核心原理在於線程池對 Thread 進行了封裝,並非每次執行任務都會調用 Thread.start() 來建立新線程,而是讓每一個線程去執行一個「循環任務」,在這個「循環任務」中不停的檢查是否有任務須要被執行,若是有則直接執行,也就是調用任務中的 run 方法,將 run 方法當成一個普通的方法執行,經過這種方式將只使用固定的線程就將全部任務的 run 方法串聯起來。

總結

本文經過沒有使用線程池帶來的弊端,Executors介紹,Executors的六種方法介紹、如何使用線程池,瞭解線程池原理,核心參數,以及10到線程池面試題。

「成功不是未來纔有的,而是從決定去作的那一刻起,持續累積而成。」

相關文章
相關標籤/搜索