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

  以前寫過JAVA中建立線程的三種方法及比較。此次來講說線程池。html

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

 

1、經過Executors類提供的方法。緩存

一、newCachedThreadPool安全

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

代碼例子:spa

 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     }

效果:線程

 

  由於初始線程池沒有線程,而線程不足會不斷新建線程,因此線程名都是不同的。日誌

 

二、newFixedThreadPool代碼規範

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

代碼例子:

 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秒輸出。

 

三、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()方法。

 

四、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秒按順序輸出的。

 

2、經過ThreadPoolExecutor類自定義。

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

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

一、共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 拋棄當前任務。

 

二、順便說下線程池的執行規則以下:

(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個線程。

 

3、五種方式的優劣比較

  說是5種方式的比較,其實就是2種方式的比較,爲何這麼說?由於Executors類提供的4種方式,其底層其實都是經過ThreadPoolExecutor類來實現的。換句話說,就是Executors類工廠經過參數的組合,組裝出了上面提到的4種類型線程池供不一樣場景使用。咱們能夠經過查看Executors類的源碼來看看:

一、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秒存活時間,從而實現了一個可緩存的線程池。

 

二、newFixedThreadPool

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

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

 

三、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方法。

 

四、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類來建立線程池,根據本身須要的場景來建立一個合適的線程池。

相關文章
相關標籤/搜索