玩轉線程池

多線程的異步執行方式,雖然能夠最大限度發揮多核計算機的計算能力,但是如果不加控制,反而會對系統造成負擔。線程本身也要佔用內存空間,大量的線程會佔用內存資源並且可能會導致Out of Memory。即便沒有這樣的情況,大量的線程回收也會給GC帶來很大的壓力。爲了避免重複的創建線程,線程池的出現可以讓線程進行復用。通俗點講,當有工作來,就會向線程池拿一個線程,當工作完成後,並不是直接關閉線程,而是將這個線程歸還給線程池供其他任務使用。

線程池整體結構

Executor是線程池最上層的接口,這個接口有一個核心方法execute(Runnable command),具體是由ThreadPoolExecutor類實現,用於執行任務,ExecutorService接口繼承Executor,提供了shutdown(),shutdownNew(),submit()等用來關閉和執行線程的方法。

ExecutorService最終的默認實現類ThreadPoolExecutor。

 

ThreadPoolExecutor分析

首先通過構造器來一步一步分析。

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

 

  • corePoolSize:線程池的核心池大小,在創建線程池之後,線程池默認沒有任何線程,創建之後,默認線程池線程數量爲0,當任務過來時就會創建一個新的線程,直到達到corePoolSize之後,會將線程放入workQueue中

  • maximumPoolSize:最大線程數量

  • workQueue一個阻塞隊列,用來存儲等待執行的任務,當線程池中的線程數超過它的corePoolSize的時候,線程會進入阻塞隊列。通過workQueue,線程池實現了阻塞功能

  • threadFactory:線程工廠,用來創建線程

  • handler:表示當拒絕處理任務時的策略

  • keepAliveTime:線程池維護線程鎖允許的空閒時間,當線程池中的線程數量大於corePoolSize的時候,如果這時沒有新的任務提交,核心線程之外的線程不會立即銷燬,而是會等待,直到等待的時間超過了keepAliveTime

    這些參數是怎麼再具體執行中發揮作用的呢,我們來看下

     

    execute的執行流程圖

     

     

 

當任務過來時就會創建一個新的線程,直到達到corePoolSize之後,會將線程放入workQueue中, 如果運行線程小於corePoolSize,即使有空閒線程,當任務過來時也會創建新的線程  如果線程池數量大於corePoolSize但小於maximumPoolSize,則只有當workQueue滿時才創建新的線程去處理任務

 如果運行線程數量大於等於maximumPoolSize時,這時如果workQueue也已經滿了,則通過handler所指定的策略去處理,如果除核心線程之外的線程沒有處理任務,且超過keepAliveTime,就會被回收。

 

四種常用線程池

 

01 newCachedThreadPool

 

   public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 工作線程的創建數量幾乎沒有限制(其實也有限制的,數目爲Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。

  • 如果長時間沒有往線程池中提交任務,即如果工作線程空閒了指定的時間(默認爲1分鐘),則該工作線程將自動終止。終止後,如果你又提交了新的任務,則線程池重新創建一個工作線程。

 

  • 在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量線程同時運行,很有會造成系統癱瘓。

     

    02 newFixedThreadPool

     

 

   public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

 

  • 創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到線程池池隊列中。

     

  • FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閒時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會佔用一定的系統資源。

     

  03 newSingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor() {        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

一個單線程的線程池,只有一個線程在工作(如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。)能夠保證所有任務的執行順序按照任務的提交順序執行,同一時段只有一個任務在運行。

 

04 newScheduleThreadPool

 

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

返回值是ScheduledExecutorService

創建一個定長的線程池,而且支持定時的以及週期性的任務執行。
可定時運行(初始延時),運行頻率(每隔多長時間運行,還是運行成功一次之後再隔多長時間再運行)的線程池
適合定時以及週期性執行任務的場合。

這裏有個執行方法需要注意的。

  private void threadPoolExecutorTest6() throws Exception {
        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
        // 週期性執行某一個任務,線程池提供了兩種調度方式,這裏單獨演示一下。測試場景一樣。
        // 測試場景:提交的任務需要3秒才能執行完畢。看兩種不同調度方式的區別
        // 效果1: 提交後,2秒後開始第一次執行,之後每間隔1秒,固定執行一次(如果發現上次執行還未完畢,則等待完畢,完畢後立刻執行)。
        // 也就是說這個代碼中是,3秒鐘執行一次(計算方式:每次執行三秒,間隔時間1秒,執行結束後馬上開始下一次執行,無需等待)
        threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任務-1 被執行,現在時間:" + System.currentTimeMillis());
            }
        }, 2000, 1000, TimeUnit.MILLISECONDS);

        // 效果2:提交後,2秒後開始第一次執行,之後每間隔1秒,固定執行一次(如果發現上次執行還未完畢,則等待完畢,等上一次執行完畢後再開始計時,等待1秒)。
        // 也就是說這個代碼鐘的效果看到的是:4秒執行一次。 (計算方式:每次執行3秒,間隔時間1秒,執行完以後再等待1秒,所以是 3+1)
        threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任務-2 被執行,現在時間:" + System.currentTimeMillis());
            }
        }, 2000, 1000, TimeUnit.MILLISECONDS);
    }

 

阻塞隊列

在線程池中,如果任務數量超過了核心線程數,就會把任務放入阻塞隊列等待運行,在線程池中主要有三種阻塞隊列

 

  • ArrayBlockingQueue :基於數組的有界隊列。

  • LinkedBlockingQueue:基於鏈表的先進先出隊列,是無界的。

  • SynchronousQueue:無緩衝等待隊列,它將任務直接交給線程處理而不保持它們。如果不存在可用於立即運行任務的線程(即線程池中的線程都在工作),則試圖把任務加入緩衝隊列將會失敗,因此會構造一個新的線程來處理新添加的任務,並將其加入到線程池中。

     

四種拒絕策略

  • AbortPolicy 丟棄任務,並拋出RejectedExecutionException 異常

  • CallerRunsPolicy:該任務被線程池拒絕,由調用 execute方法的線程執行該任務。

  • DiscardOldestPolicy :拋棄隊列最前面的任務,然後重新嘗試執行任務。

  • DiscardPolicy:丟棄任務,不過也不拋出異常。

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