Java線程池的正確使用方式——不要再new Thread了

這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰html

背景

最近在實驗室作相關工做時,一個小夥伴看見項目代碼中出現了 new Thread(...) ,破口大罵之。看見這一場景,我默默地刪掉了我在另外一個地方寫的 new Thread(...) 看成無事發生(還好他沒看見XD)。java

爲了避免再犯這種錯誤,我寫下這篇文章來記錄一下Java線程究竟該怎麼使用(纔不會被罵),也是開了一個新坑!git

若有錯誤歡迎聯繫我指正!api

爲何不要用new Thread

首先從我秉持的原則入手,「簡潔優雅」。試想若是在一段代碼中你須要建立不少線程,那麼你就不停地調用 new Thread(...).start() 麼?顯然這樣的代碼一點也不簡潔,也不優雅。初次以外這樣的代碼還有不少壞處:緩存

  1. 每次都要新建一個對象,性能差;
  2. 建出來的不少個對象是獨立的,缺少統一的管理。若是在代碼中無限新建線程會致使這些線程相互競爭,佔用過多的系統資源從而致使死機或者 oom
  3. 缺少許多功能如定時執行、中斷等。

從這些壞處很容易能夠看出解決方法,那就是弄一個監管者來統一的管理這些線程,並將它們存到一個集合(或者相似的數據結構)中,並且還要動態地分配它們的任務。固然Java已經給咱們提供好十分健全的東西來使用了,那就是線程池markdown

Java線程池

Java提供了一個工廠類來構造咱們須要的線程池,這個工廠類就是 Executors 。這個類提供了不少方法,咱們這裏主要講它提供的4個建立線程池的方法,即數據結構

  • newCachedThreadPool()
  • newFixedThreadPool(int nThreads)
  • newScheduledThreadPool(int corePoolSize)
  • newSingleThreadExecutor()

newCachedThreadPool()

這個方法正如它的名字同樣,建立緩存線程池。緩存的意思就是這個線程池會根據須要建立新的線程,在有新任務的時候會優先使用先前建立出的線程。也就是說線程一旦建立了就一直在這個池子裏面了,執行完任務後後續還有任務須要會重用這個線程,如果線程不夠用了再去新建線程多線程

以一段代碼作個例子:併發

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 建立緩存線程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 每次發佈任務前等待一段時間,如1s
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 執行任務
    cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}
複製代碼

在這個例子裏,我在每次調用線程執行任務以前都等待1秒,這使時間讓線程池內的線程執行完上一個任務綽綽有餘,因此你會發現輸出裏都是同一個線程在執行任務。oracle

cachepool1.png

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 建立緩存線程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 每次發佈任務前根據奇偶不一樣等待一段時間,如1s
    if (i % 2 == 0) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 執行任務
    cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}
複製代碼

這個例子中我在每次調用線程執行任務以前根據奇偶不一樣控制其是否等待,這樣就會在同一時間須要執行2個任務,因此線程池中按須要多建立了一個線程。你也能夠把這個模數改大到三、四、5...來觀察線程池是否按需建立了新線程。

cachepool2.png

注意這裏的線程池是無限大的,咱們並無規定他的大小。(但其實在實際使用時不多是無限大的,我會在這個系列後面的文章再來探討這個問題)

newFixedThreadPool(int nThreads)

能夠看到這個方法中帶了一個參數,這個方法建立的線程池是定長的,這個參數就是線程池的大小。也就是說,在同一時間執行的線程數量只能是 nThreads 這麼多,這個線程池能夠有效的控制最大併發數從而防止佔用過多資源。超出的線程會放在線程池的一個隊列裏等待其餘線程執行完,這個隊列也是值得咱們去好好研究的,它是一個無界隊列,我會在這個系列後面的文章探討它。

以一段代碼作個例子:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); // 建立緩存線程池,大小爲3

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 執行任務
    fixedThreadPool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + index);

        // 模擬執行任務耗時1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
複製代碼

這個例子裏能夠看到我建立了一個大小爲3的線程池,也就是說它支持的最大併發線程數是3,運行後發現這些數確實是3個3個爲一組輸出的。

fixedpool1.png

合理的設置定長線程池的大小是一個很重要的事情。

newScheduledThreadPool(int corePoolSize)

從 Scheduled 大概能夠猜出這個線程池是爲了解決上面說過的第3個壞處,也就是缺少定時執行功能。這個線程池也是定長的,參數 corePoolSize 就是線程池的大小,即在空閒狀態下要保留在池中的線程數量。

而要實現調度須要使用這個線程池的 schedule() 方法 (注意這裏要把新建線程池的返回類 ExecutorService 改爲 ScheduledExecutorService 噢)

以一段代碼作個例子:

// 注意!這裏把 ExecutorService 改爲了 ScheduledExecutorService ,不然沒有定時功能
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); // 建立緩存線程池

// 執行任務
scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + ": 我會在3秒後執行。"),
        3, TimeUnit.SECONDS);
複製代碼

這個例子會在3秒後輸出結果。固然你能夠根據不一樣的需求設置不一樣的定時,甚至還能實現按期執行功能,詳細能夠查看[官方api]

scheduledpool1.png

newSingleThreadExecutor()

這個線程池就比較簡單了,他是一個單線程池,只使用一個線程來執行任務。可是它與 newFixedThreadPool(1, threadFactory) 不一樣,它會保證建立的這個線程池不會被從新配置爲使用其餘的線程,也就是說這個線程池裏的線程始終如一。

以一段代碼作個例子:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 建立單線程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 執行任務
    singleThreadExecutor.execute(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + index);

        // 模擬執行任務耗時1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
複製代碼

能夠看到輸出裏他只會一秒一秒地打印內容,只有一個線程在執行任務。

singlethreadpool1.png

線程池的關閉

若是你運行了我上面的示例,你會發現程序一直都沒有結束,這是由於我上面的示例代碼並無關閉線程池。線程池自己提供了兩個關閉的方法:

  • shutdown() : 將線程池狀態置成 SHUTDOWN,此時再也不接受新的任務等待線程池中已有任務執行完成後結束
  • shutdownNow() : 將線程池狀態置成 SHUTDOWN,將線程池中全部線程中斷(調用線程的 interrupt() 操做),清空隊列,並返回正在等待執行的任務列表

而且它還提供了查看線程池是否關閉和是否終止的方法,分別爲 isShutdown()isTerminated()

總結

那麼根據須要使用以上四種線程池就足夠應對平時的需求了,別再使用 new Thread(...) 這種方法啦!

固然,線程池只能隱式的控制線程變量,若是有業務需求須要對線程進行定製化的監控控制,那也請絕不吝嗇的使用new Thread(...)

相關文章
相關標籤/搜索