Java 創建線程池的方式


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();
            }
        });
    }
}

運行效果:

image-20211012153250405

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

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();
        }
    });
}

運行效果:

image-20211012153559224

因為線程池大小是固定的,這里設置的是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);
    }
}

運行效果:

image-20211012155541924

因為設置了延遲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();
            }
        });
    }
}

運行效果:

image-20211012155858560

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

2、通過ThreadPoolExecutor類自定義(推薦)

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

image-20211012161434990

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();
            }
        });
    }
}

運行效果:

image-20211012162705435

因為核心線程數為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類來創建線程池,根據自己需要的場景來創建一個合適的線程池。

參考資料https://www.cnblogs.com/pcheng/p/13540619.html


免責聲明!

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



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