Java 創建線程池的方式
Java 創建線程池主要有兩種方法,一種是通過 Executors 工廠類提供的方法,該類提供了4種不同的線程池;另一種是通過 ThreadPoolExecutor類進行自定義創建。
1、通過 Executors 工廠類提供的方法
1.1、newCachedThreadPool
創建一個可緩存的線程池,若線程數超過處理所需,緩存一段時間后會回收,若線程數不夠,則新建線程。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int number = i;
executorService.execute(() -> {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
運行效果:
因為初始線程池沒有線程,而線程不足會不斷新建線程,所以線程名都是不一樣的。
1.2、newFixedThreadPool
創建一個固定大小的線程池,可控制並發的線程數,超出的線程會在隊列中等待。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
int number = i;
executorService.execute(() -> {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
運行效果:
因為線程池大小是固定的,這里設置的是3個線程,所以線程名只有3個。因為線程不足會進入隊列等待線程空閑,所以日志間隔2秒輸出。
1.3、newScheduledThreadPool
創建一個周期性的線程池,支持定時及周期性執行任務。
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println(LocalDateTime.now() + " " + "執行任務");
for (int i = 0; i < 10; i++) {
int number = i;
executorService.schedule(() -> {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},3, TimeUnit.SECONDS);
}
}
運行效果:
因為設置了延遲3秒,所以提交后3秒才開始執行任務。因為這里設置核心線程數為3個,而線程不足會進入隊列等待線程空閑,所以日志間隔2秒輸出。
注意:這里用的是ScheduledExecutorService類的schedule()方法,不是ExecutorService類的execute()方法。
1.4、newSingleThreadExecutor
創建一個單線程的線程池,可保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
int number = i;
executorService.execute(() -> {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
運行效果:
因為只有一個線程,所以線程名均相同,且是每隔2秒按順序輸出的。
2、通過ThreadPoolExecutor類自定義(推薦)
ThreadPoolExecutor類提供了4種構造方法,可根據需要來自定義一個線程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//省略
}
2.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.2、線程池執行規則
線程池的執行規則如下:
(1)當線程數小於核心線程數時,創建線程。
(2)當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
(3)當線程數大於等於核心線程數,且任務隊列已滿:
若線程數小於最大線程數,創建線程。
若線程數等於最大線程數,拋出異常,拒絕任務。
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(2, 10, 1, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(5, true), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
int number = i;
executorService.execute(() -> {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " " + number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
運行效果:
因為核心線程數為2,隊列大小為5,存活時間1分鍾,所以流程是第0-1號任務來時,陸續創建2個線程,然后第2-6號任務來時,因為無線程可用,均進入了隊列等待,第7-9號任務來時,沒有空閑線程,隊列也滿了,所以陸續又創建了3個線程。所以你會發現7-9號任務反而是先執行的。又因為各任務只需要2秒,而線程存活時間有1分鍾,所以線程進行了復用,所以總共只創建了5個線程。
3、優劣比較
雖然看上去 Executors 類的封裝,可以簡化我們的使用,但事實上,阿里代碼規范《阿里巴巴Java開發手冊》中明確不建議使用 Executors 類提供的這4種方法:
【強制】線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
Executors返回的線程池對象的弊端如下:
FixedThreadPool和SingleThreadPool:允許的請求隊列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。
CachedThreadPool和ScheduledThreadPool:允許的創建線程數量為Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。
所以我們應該使用ThreadPoolExecutor類來創建線程池,根據自己需要的場景來創建一個合適的線程池。