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