java 線程池看這一篇就行了

線程池介紹

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

爲什麼要使用線程池?

  • 可重複使用已有線程,避免對象創建、消亡和過度切換的性能開銷。

  • 避免創建大量同類線程所導致的資源過度競爭和內存溢出的問題。

  • 支持更多功能,比如延遲任務線程池(newScheduledThreadPool)和緩存線程池(newCachedThreadPool)等。

線程池使用

創建線程池有兩種方式:ThreadPoolExecutor 和 Executors,其中 Executors 又可以創建 6 種不同的線程池類型,會在下節講,本節重點來看看 ThreadPoolExecutor 的使用。

ThreadPoolExecutor 的使用

線程池使用代碼如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        // 執行線程池
        System.out.println("Hello, Java.");
    }
});

以上程序執行結果如下:

Hello, Java.

ThreadPoolExecutor 參數說明

ThreadPoolExecutor 構造方法有以下四個,如下圖所示:

enter image description here

其中最後一個構造方法有 7 個構造參數,包含了前三個方法的構造參數,這 7 個參數名稱如下所示:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    //...
}

其代表的含義如下:

① corePoolSize

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

② maximumPoolSize

線程池中最大線程數,如果活動的線程達到這個數值以後,後續的新任務將會被阻塞(放入任務隊列)。

③ keepAliveTime

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

④ unit

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

⑤ workQueue

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

⑥ threadFactory

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

⑦ rejectedExecutionHandler

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

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

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

  • new ThreadPoolExecutor.AbortPolicy():直接拋出 RejectedExecutionException 異常

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

包含所有參數的 ThreadPoolExecutor 使用代碼:

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

線程池執行方法 execute() VS 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("Hello, Java.");
    }
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("Hello, 老王.");
        return "Success";
    }
});
System.out.println(future.get());

以上程序執行結果如下:

Hello, Java.

Hello, 老王.

Success

線程池關閉

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

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

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

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

threadPoolExecutor.execute(() -> {
    for (int i = 0; i < 2; i++) {
        System.out.println("I'm " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }
});
threadPoolExecutor.shutdown();
threadPoolExecutor.execute(() -> {
    System.out.println("I'm Java.");
});

以上程序執行結果如下:

I'm 0

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.interview.chapter5.Section2$$Lambda$2/1828972342@568db2f2 rejected from java.util.concurrent.ThreadPoolExecutor@378bf509[Shutting down, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]

I'm 1

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

相關面試題

1.ThreadPoolExecutor 有哪些常用的方法?

答:常用方法如下所示:

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

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

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

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

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

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

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

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

2.以下程序執行的結果是什麼?

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue());
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println("I:" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
});
threadPoolExecutor.shutdownNow();
System.out.println("Java");

答:程序執行的結果是:

I:0

Java

java.lang.InterruptedException: sleep interrupted(報錯信息)

I:1

題目解析:因爲程序中使用了 shutdownNow() 會導致程序執行一次之後報錯,拋出 sleep interrupted 異常,又因爲本身有 try/catch,所以程序會繼續執行打印 I:1 。

3.在 ThreadPool 中 submit() 和 execute() 有什麼區別?

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

submit() 方法源碼(JDK 8)如下:

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

execute() 源碼(JDK 8)如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    //..... 其他
}

4.說一下 ThreadPoolExecutor 都需要哪些參數?

答:ThreadPoolExecutor 最多包含以下七個參數:

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

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

  • keepAliveTime:閒置超時時間

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

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

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

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

更多詳細介紹,請見正文。

5.在線程池中 shutdownNow() 和 shutdown() 有什麼區別?

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

6.說一說線程池的工作原理?

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

7.以下線程名稱被打印了幾次?

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
                new ThreadPoolExecutor.DiscardPolicy());
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();
            }
        }
    });

答:線程名被打印了 3 次。
題目解析:線程池第 1 次執行任務時,會新創建任務並執行;第 2 次執行任務時,因爲沒有空閒線程所以會把任務放入隊列;第 3 次同樣把任務放入隊列,因爲隊列最多可以放兩條數據,所以第 4 次之後的執行都會被捨棄(沒有定義拒絕策略),於是就打印了 3 次線程名稱。

總結

ThreadPoolExecutor 是創建線程池最傳統和最推薦使用的方式,創建時要設置線程池的核心線程數和最大線程數還有任務隊列集合,如果任務量大於隊列的最大長度,線程池會先判斷當前線程數量是否已經到達最大線程數,如果沒有達到最大線程數就新建線程來執行任務,如果已經達到最大線程數,就會執行拒絕策略(拒絕策略可自行定義)。線程池可通過 submit() 來調用執行,從而獲得線程執行的結果,也可以通過 shutdown() 來終止線程池。

下一篇:Java 線程池

在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈

往期精選

分佈式數據之緩存技術,一起來揭開其神祕面紗

分佈式數據複製技術,今天就教你真正分身術

數據分佈方式之哈希與一致性哈希,我就是個神算子

分佈式存儲系統三要素,掌握這些就離成功不遠了

想要設計一個好的分佈式系統,必須搞定這個理論

分佈式通信技術之發佈訂閱,乾貨滿滿

分佈式通信技術之遠程調用:RPC

消息隊列Broker主從架構詳細設計方案,這一篇就搞定主從架構

消息中間件路由中心你會設計嗎,不會就來學學

消息隊列消息延遲解決方案,跟着做就行了

秒殺系統每秒上萬次下單請求,我們該怎麼去設計

【分佈式技術】分佈式系統調度架構之單體調度,非掌握不可

CDN加速技術,作爲開發的我們真的不需要懂嗎?

煩人的緩存穿透問題,今天教就你如何去解決

分佈式緩存高可用方案,我們都是這麼幹的

每天百萬交易的支付系統,生產環境該怎麼設置JVM堆內存大小

你的成神之路我已替你鋪好,沒鋪你來捶我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章