JAVA中創建線程池的五種方法及比較


  之前寫過JAVA中創建線程的三種方法及比較。這次來說說線程池。

  JAVA中創建線程池主要有兩類方法,一類是通過Executors工廠類提供的方法,該類提供了4種不同的線程池可供使用。另一類是通過ThreadPoolExecutor類進行自定義創建。

 

一、通過Executors類提供的方法。

1、newCachedThreadPool

創建一個可緩存的線程池,若線程數超過處理所需,緩存一段時間后會回收,若線程數不夠,則新建線程。

代碼例子:

 1     private static void createCachedThreadPool() {
 2         ExecutorService executorService = Executors.newCachedThreadPool();
 3         for (int i = 0; i < 10; i++) {
 4             final int index = i;
 5             executorService.execute(() -> {
 6                 // 獲取線程名稱,默認格式:pool-1-thread-1
 7                 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
 8                 // 等待2秒
 9                 sleep(2000);
10             });
11         }
12     }

效果:

 

  因為初始線程池沒有線程,而線程不足會不斷新建線程,所以線程名都是不一樣的。

 

2、newFixedThreadPool

創建一個固定大小的線程池,可控制並發的線程數,超出的線程會在隊列中等待。

代碼例子:

 1     private static void createFixedThreadPool() {
 2         ExecutorService executorService = Executors.newFixedThreadPool(3);
 3         for (int i = 0; i < 10; i++) {
 4             final int index = i;
 5             executorService.execute(() -> {
 6                 // 獲取線程名稱,默認格式:pool-1-thread-1
 7                 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
 8                 // 等待2秒
 9                 sleep(2000);
10             });
11         }
12     }

效果:

 

  因為線程池大小是固定的,這里設置的是3個線程,所以線程名只有3個。因為線程不足會進入隊列等待線程空閑,所以日志間隔2秒輸出。

 

3、newScheduledThreadPool

創建一個周期性的線程池,支持定時及周期性執行任務。

代碼例子:

 1     private static void createScheduledThreadPool() {
 2         ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
 3         System.out.println(DateUtil.now() + " 提交任務");
 4         for (int i = 0; i < 10; i++) {
 5             final int index = i;
 6             executorService.schedule(() -> {
 7                 // 獲取線程名稱,默認格式:pool-1-thread-1
 8                 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
 9                 // 等待2秒
10                 sleep(2000);
11             }, 3, TimeUnit.SECONDS);
12         }
13     }

效果:

 

  因為設置了延遲3秒,所以提交后3秒才開始執行任務。因為這里設置核心線程數為3個,而線程不足會進入隊列等待線程空閑,所以日志間隔2秒輸出。

注意:這里用的是ScheduledExecutorService類的schedule()方法,不是ExecutorService類的execute()方法。

 

4、newSingleThreadExecutor

創建一個單線程的線程池,可保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

代碼例子:

 1     private static void createSingleThreadPool() {
 2         ExecutorService executorService = Executors.newSingleThreadExecutor();
 3         for (int i = 0; i < 10; i++) {
 4             final int index = i;
 5             executorService.execute(() -> {
 6                 // 獲取線程名稱,默認格式:pool-1-thread-1
 7                 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
 8                 // 等待2秒
 9                 sleep(2000);
10             });
11         }
12     }

效果:

 

  因為只有一個線程,所以線程名均相同,且是每隔2秒按順序輸出的。

 

二、通過ThreadPoolExecutor類自定義。

  ThreadPoolExecutor類提供了4種構造方法,可根據需要來自定義一個線程池。

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

1、共7個參數如下:

(1)corePoolSize:核心線程數,線程池中始終存活的線程數。

(2)maximumPoolSize: 最大線程數,線程池中允許的最大線程數。

(3)keepAliveTime: 存活時間,線程沒有任務執行時最多保持多久時間會終止。

(4)unit: 單位,參數keepAliveTime的時間單位,7種可選。

參數 描述
TimeUnit.DAYS
TimeUnit.HOURS 小時
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS 毫秒
TimeUnit.MICROSECONDS 微妙
TimeUnit.NANOSECONDS 納秒

(5)workQueue: 一個阻塞隊列,用來存儲等待執行的任務,均為線程安全,7種可選。

參數 描述
ArrayBlockingQueue 一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue 一個由鏈表結構組成的有界阻塞隊列。
SynchronousQueue 一個不存儲元素的阻塞隊列,即直接提交給線程不保持它們。
PriorityBlockingQueue 一個支持優先級排序的無界阻塞隊列。
DelayQueue 一個使用優先級隊列實現的無界阻塞隊列,只有在延遲期滿時才能從中提取元素。
LinkedTransferQueue 一個由鏈表結構組成的無界阻塞隊列。與SynchronousQueue類似,還含有非阻塞方法。
LinkedBlockingDeque 一個由鏈表結構組成的雙向阻塞隊列。

較常用的是LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。

(6)threadFactory: 線程工廠,主要用來創建線程,默及正常優先級、非守護線程。

(7)handler:拒絕策略,拒絕處理任務時的策略,4種可選,默認為AbortPolicy。

參數 描述
AbortPolicy 拒絕並拋出異常。
CallerRunsPolicy 重試提交當前的任務,即再次調用運行該任務的execute()方法。
DiscardOldestPolicy 拋棄隊列頭部(最舊)的一個任務,並執行當前任務。
DiscardPolicy 拋棄當前任務。

 

2、順便說下線程池的執行規則如下:

(1)當線程數小於核心線程數時,創建線程。

(2)當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。

(3)當線程數大於等於核心線程數,且任務隊列已滿:

若線程數小於最大線程數,創建線程。

若線程數等於最大線程數,拋出異常,拒絕任務。

 

代碼例子:

 1     private static void createThreadPool() {
 2         ExecutorService executorService = new ThreadPoolExecutor(2, 10,
 3                 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5, true),
 4                 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
 5         for (int i = 0; i < 10; i++) {
 6             final int index = i;
 7             executorService.execute(() -> {
 8                 // 獲取線程名稱,默認格式:pool-1-thread-1
 9                 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
10                 // 等待2秒
11                 sleep(2000);
12             });
13         }
14     }

效果:

 

  因為核心線程數為2,隊列大小為5,存活時間1分鍾,所以流程是第0-1號任務來時,陸續創建2個線程,然后第2-6號任務來時,因為無線程可用,均進入了隊列等待,第7-9號任務來時,沒有空閑線程,隊列也滿了,所以陸續又創建了3個線程。所以你會發現7-9號任務反而是先執行的。又因為各任務只需要2秒,而線程存活時間有1分鍾,所以線程進行了復用,所以總共只創建了5個線程。

 

三、五種方式的優劣比較

  說是5種方式的比較,其實就是2種方式的比較,為什么這么說?因為Executors類提供的4種方式,其底層其實都是通過ThreadPoolExecutor類來實現的。換句話說,就是Executors類工廠通過參數的組合,組裝出了上面提到的4種類型線程池供不同場景使用。我們可以通過查看Executors類的源碼來看看:

1、newCachedThreadPool

1     public static ExecutorService newCachedThreadPool() {
2         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
3                                       60L, TimeUnit.SECONDS,
4                                       new SynchronousQueue<Runnable>());
5     }

  因為SynchronousQueue隊列不保持它們,直接提交給線程,相當於隊列大小為0,而最大線程數為Integer.MAX_VALUE,所以線程不足時,會一直創建新線程,等到線程空閑時,又有60秒存活時間,從而實現了一個可緩存的線程池。

 

2、newFixedThreadPool

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

  因為核心線程數與最大線程數相同,所以線程池的線程數是固定的,而且沒有限制隊列的大小,所以多余的任務均會被放到隊列排隊,從而實現一個固定大小,可控制並發數量的線程池。

 

3、newScheduledThreadPool

1     public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
2         return new ScheduledThreadPoolExecutor(corePoolSize);
3     }
4 
5     public ScheduledThreadPoolExecutor(int corePoolSize) {
6         super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
7               new DelayedWorkQueue());
8     }

  因為使用了延遲隊列,只有在延遲期滿時才能從中提取到元素,從而實現定時執行的線程池。而周期性執行是配合上層封裝的其他類來實現的,可以看ScheduledExecutorService類的scheduleAtFixedRate方法。

 

4、newSingleThreadExecutor

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

  因為核心線程數與最大線程數相同,均為1,所以線程池的線程數是固定的1個,而且沒有限制隊列的大小,所以多余的任務均會被放到隊列排隊,從而實現一個單線程按指定順序執行的線程池。

 

  雖然看上去Executors類的封裝,可以簡化我們的使用,但事實上,阿里代碼規范《阿里巴巴Java開發手冊》中明確不建議使用Executors類提供的這4種方法:

【強制】線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

Executors返回的線程池對象的弊端如下:

FixedThreadPool和SingleThreadPool:允許的請求隊列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。

CachedThreadPool和ScheduledThreadPool:允許的創建線程數量為Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。

  再回頭看看上面的源碼,確實如此。所以我們應該使用ThreadPoolExecutor類來創建線程池,根據自己需要的場景來創建一個合適的線程池。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM