之前寫過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類來創建線程池,根據自己需要的場景來創建一個合適的線程池。