java線程池的正確使用

首先我們來看一下如下方式存在的問題

   new Thread(){
         @Override
         public void run() {
              super.run();
         }
     }.start();
  • 首先頻繁的創建、銷燬對象是一個很消耗性能的事情;

  • 如果用戶量比較大,導致佔用過多的資源,可能會導致我們的服務由於資源不足而宕機;

所以實際開發中,我們並不推薦這樣直接創建線程。我們應該使用線程池來統一管理線程的創建與銷燬。

一、線程池簡單介紹

線程池,本質上是一種對象池,用於管理線程資源。
在任務執行前,需要從線程池中拿出線程來執行。
在任務執行完成之後,需要把線程放回線程池。
通過線程的這種反覆利用機制,可以有效地避免直接創建線程所帶來的壞處。

二、線程池優點

  • 降低資源的消耗。線程本身是一種資源,創建和銷燬線程會有CPU開銷;創建的線程也會佔用一定的內存。

  • 提高任務執行的響應速度。任務執行時,可以不必等到線程創建完之後再執行。

  • 提高線程的可管理性。線程不能無限制地創建,需要進行統一的分配、調優和監控。

三、Executors

javaz中爲我們封裝了一個Executors線程池工廠,提供了很多的工廠方法,方便我們快速創建線程池。

// 創建單一線程的線程池
public static ExecutorService newSingleThreadExecutor();
// 創建固定數量的線程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 創建帶緩存的線程池
public static ExecutorService newCachedThreadPool();
// 創建定時調度的線程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 創建流式(fork-join)線程池
public static ExecutorService newWorkStealingPool();

四、如何正確使用線程池

不要使用Executors.newXXXThreadPool()快捷方法創建線程池,因爲這種方式會使用無界的任務隊列,爲避免OOM,我們應該使用ThreadPoolExecutor的構造方法手動指定隊列的最大長度。

ThreadPoolExecutor提供的構造函數

//五個參數的構造函數

public ThreadPoolExecutor(int corePoolSize,

                          int maximumPoolSize,

                          long keepAliveTime,

                          TimeUnit unit,

                          BlockingQueue<Runnable> workQueue)

//六個參數的構造函數-1

public ThreadPoolExecutor(int corePoolSize,

                          int maximumPoolSize,

                          long keepAliveTime,

                          TimeUnit unit,

                          BlockingQueue<Runnable> workQueue,

                          ThreadFactory threadFactory)

//六個參數的構造函數-2

public ThreadPoolExecutor(int corePoolSize,

                          int maximumPoolSize,

                          long keepAliveTime,

                          TimeUnit unit,

                          BlockingQueue<Runnable> workQueue,

                          RejectedExecutionHandler handler)

//七個參數的構造函數

public ThreadPoolExecutor(int corePoolSize,

                          int maximumPoolSize,

                          long keepAliveTime,

                          TimeUnit unit,

                          BlockingQueue<Runnable> workQueue,

                          ThreadFactory threadFactory,

                          RejectedExecutionHandler handler)

  • corePoolSize,線程池中的核心線程數
  • maximumPoolSize,線程池中的最大線程數
  • keepAliveTime,空閒時間,當線程池數量超過核心線程數時,多餘的空閒線程存活的時間,即:這些線程多久被銷燬。
  • unit,空閒時間的單位,可以是毫秒、秒、分鐘、小時和天,等等
  • workQueue,等待隊列,線程池中的線程數超過核心線程數時,任務將放在等待隊列,它是一個BlockingQueue類型的對象
  • threadFactory,線程工廠,我們可以使用它來創建一個線程
  • handler,拒絕策略,當線程池和等待隊列都滿了之後,需要通過該對象的回調函數進行回調處理

基本瞭解了這幾個參數再來看看實際的運用。

通常我們都是使用:

threadPool.execute(new Job());

這樣的方式來提交一個任務到線程池中,所以核心的邏輯就是 execute() 函數了。

在具體分析之前先了解下線程池中所定義的狀態,這些狀態都和線程的執行密切相關:

222.jpg

  • RUNNING 自然是運行狀態,指可以接受任務執行隊列裏的任務
  • SHUTDOWN 指調用了 shutdown() 方法,不再接受新任務了,但是隊列裏的任務得執行完畢。
  • STOP 指調用了 shutdownNow() 方法,不再接受新任務,同時拋棄阻塞隊列裏的所有任務並中斷所有正在執行任務。
  • TIDYING 所有任務都執行完畢,在調用 shutdown()/shutdownNow() 中都會嘗試更新爲這個狀態。
  • TERMINATED 終止狀態,當執行 terminated() 後會更新爲這個狀態。
    444.jpg

然後看看 execute() 方法是如何處理的:
111.jpg

  1. 獲取當前線程池的狀態。
  2. 當前線程數量小於 coreSize 時創建一個新的線程運行。
  3. 如果當前線程處於運行狀態,並且寫入阻塞隊列成功。
  4. 雙重檢查,再次獲取線程狀態;如果線程狀態變了(非運行狀態)就需要從阻塞隊列移除任務,並嘗試判斷線程是否全部執行完畢。同時執行拒絕策略。
  5. 如果當前線程池爲空就新創建一個線程並執行。
  6. 如果在第三步的判斷爲非運行狀態,嘗試新建線程,如果失敗則執行拒絕策略。
    這裏藉助《聊聊併發》的一張圖來描述這個流程:
    333.jpg

五、線程池和裝修公司

以運營一家裝修公司做個比喻。公司在辦公地點等待客戶來提交裝修請求;公司有固定數量的正式工以維持運轉;旺季業務較多時,新來的客戶請求會被排期,比如接單後告訴用戶一個月後才能開始裝修;當排期太多時,爲避免用戶等太久,公司會通過某些渠道(比如人才市場、熟人介紹等)僱傭一些臨時工(注意,招聘臨時工是在排期排滿之後);如果臨時工也忙不過來,公司將決定不再接收新的客戶,直接拒單。

線程池就是程序中的“裝修公司”,代勞各種髒活累活。上面的過程對應到線程池上:

// Java線程池的完整構造函數
public ThreadPoolExecutor(
  int corePoolSize, // 正式工數量
  int maximumPoolSize, // 工人數量上限,包括正式工和臨時工
  long keepAliveTime, TimeUnit unit, // 臨時工遊手好閒的最長時間,超過這個時間將被解僱
  BlockingQueue<Runnable> workQueue, // 排期隊列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒單方式

這些參數裏面,基本類型的參數都比較簡單,我們不做進一步的分析。我們更關心的是workQueue、threadFactory和handler,接下來我們將進一步分析。

1. 等待隊列-workQueue

等待隊列是BlockingQueue類型的,理論上只要是它的子類,我們都可以用來作爲等待隊列。

jdk內部自帶一些阻塞隊列

名稱 簡介
ArrayBlockingQueue 隊列是有界的,基於數組實現的阻塞隊列
LinkedBlockingQueue 隊列可以有界,也可以無界。基於鏈表實現的阻塞隊列
SynchronousQueue 不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作將一直處於阻塞狀態。該隊列也是Executors.newCachedThreadPool()的默認隊列
PriorityBlockingQueue 帶優先級的無界阻塞隊列通常情況下,我們需要指定阻塞隊列的上界(比如1024)。另外,如果執行的任務很多,我們可能需要將任務進行分類,然後將不同分類的任務放到不同的線程池中執行

2. 線程工廠-threadFactory

ThreadFactory是一個接口,只有一個方法。既然是線程工廠,那麼我們就可以用它生產一個線程對象。來看看這個接口的定義。

public interface ThreadFactory {

    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

Executors的實現使用了默認的線程工廠-DefaultThreadFactory。它的實現主要用於創建一個線程,線程的名字爲pool-{poolNum}-thread-{threadNum}。

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

很多時候,我們需要自定義線程名字。我們只需要自己實現ThreadFactory,用於創建特定場景的線程即可。

3. 拒絕策略-handler

所謂拒絕策略,就是當線程池滿了、隊列也滿了的時候,我們對任務採取的措施。或者丟棄、或者執行、或者其他…

線程池給我們提供了四種常見的拒絕策略:

拒絕策略 拒絕行爲
AbortPolicy 拋出RejectedExecutionException
DiscardPolicy 什麼也不做,直接忽略
DiscardOldestPolicy 丟棄執行隊列中最老的任務,嘗試爲當前提交的任務騰出位置
CallerRunsPolicy 直接由提交任務者執行這個任務

這四種策略各有優劣,比較常用的是DiscardPolicy,但是這種策略有一個弊端就是任務執行的軌跡不會被記錄下來。所以,我們往往需要實現自定義的拒絕策略, 通過實現RejectedExecutionHandler接口的方式。

六、提交任務的幾種方式

往線程池中提交任務,主要有兩種方法,execute()和submit()。

execute()用於提交不需要返回結果的任務

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.execute(() -> System.out.println("hello"));
}
  • submit()用於提交一個需要返回果的任務。
  • 該方法返回一個Future對象,通過調用這個對象的get()方法,我們就能獲得返回結果。get()方法會一直阻塞,直到返回結果返回。
  • 我們也可以使用它的重載方法get(long timeout, TimeUnit unit),這個方法也會阻塞,但是在超時時間內仍然沒有返回結果時,將拋出異常TimeoutException。
public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    Future<Long> future = executor.submit(() -> {
        System.out.println("task is executed");
        return System.currentTimeMillis();
    });
    System.out.println("task execute time is: " + future.get());
}

七、關閉線程池

在線程池使用完成之後,我們需要對線程池中的資源進行釋放操作,這就涉及到關閉功能。我們可以調用線程池對象的shutdown()和shutdownNow()方法來關閉線程池。

這兩個方法都是關閉操作,又有什麼不同呢?

  1. shutdown()會將線程池狀態置爲SHUTDOWN,不再接受新的任務,同時會等待線程池中已有的任務執行完成再結束。
  2. shutdownNow()會將線程池狀態置爲SHUTDOWN,對所有線程執行interrupt()操作,清空隊列,並將隊列中的任務返回回來。
    另外,關閉線程池涉及到兩個返回boolean的方法,isShutdown()和isTerminated,分別表示是否關閉和是否終止。

八、如何正確配置線程池的參數

前面我們講到了手動創建線程池涉及到的幾個參數,那麼我們要如何設置這些參數纔算是正確的應用呢?實際上,需要根據任務的特性來分析。

  1. 任務的性質:CPU密集型、IO密集型和混雜型
  2. 任務的優先級:高中低
  3. 任務執行的時間:長中短
  4. 任務的依賴性:是否依賴數據庫或者其他系統資源

不同的性質的任務,我們採取的配置將有所不同。在《Java併發編程實踐》中有相應的計算公式。

通常來說,如果任務屬於CPU密集型,那麼我們可以將線程池數量設置成CPU的個數,以減少線程切換帶來的開銷。如果任務屬於IO密集型,我們可以將線程池數量設置得更多一些,比如CPU個數*2。

PS:我們可以通過Runtime.getRuntime().availableProcessors()來獲取CPU的個數。

總結

  • 儘量使用手動的方式創建線程池,避免使用Executors工廠類
  • 根據場景,合理設置線程池的各個參數,包括線程池數量、隊列、線程工廠和拒絕策略
  • 在調線程池submit()方法的時候,一定要儘量避免任務執行異常被吞掉的問題

參考鏈接
https://www.jianshu.com/p/7ab4ae9443b9
https://www.cnblogs.com/CarpenterLee/p/9558026.html
https://cloud.tencent.com/developer/article/1527294

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