天天看點

工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

前言

根據摩爾定律所說:內建電路上可容納的半導體數量每 18 個月翻一番,是以 CPU 上的半導體數量會越來越多。

但随着時間的推移,內建電路上可容納的半導體數量已趨向飽和,摩爾定律也漸漸失效,是以多核 CPU 逐漸變為主流,與之相對應的多線程程式設計也開始變得普及和流行起來,這當然也是很久之前的事了,對于現在而言多線程程式設計已經成為程式員必備的職業技能了,那接下來我們就來盤一盤“線程池”這個多線程程式設計中最重要的話題。

什麼是線程池?

線程池(ThreadPool)是一種基于池化思想管理和使用線程的機制。它是将多個線程預先存儲在一個“池子”内,當有任務出現時可以避免重新建立和銷毀線程所帶來性能開銷,隻需要從“池子”内取出相應的線程執行對應的任務即可。

Java面試筆記共享位址:Java并發程式設計面試資料集錦

池化思想在計算機的應用也比較廣泛,比如以下這些:

  • 記憶體池(Memory Pooling):預先申請記憶體,提升申請記憶體速度,減少記憶體碎片。
  • 連接配接池(Connection Pooling):預先申請資料庫連接配接,提升申請連接配接的速度,降低系統的開銷。
  • 執行個體池(Object Pooling):循環使用對象,減少資源在初始化和釋放時的昂貴損耗。

線程池的優勢主要展現在以下 4 點:

  1. 降低資源消耗:通過池化技術重複利用已建立的線程,降低線程建立和銷毀造成的損耗。
  2. 提高響應速度:任務到達時,無需等待線程建立即可立即執行。
  3. 提高線程的可管理性:線程是稀缺資源,如果無限制建立,不僅會消耗系統資源,還會因為線程的不合理分布導緻資源排程失衡,降低系統的穩定性。使用線程池可以進行統一的配置設定、調優和監控。
  4. 提供更多更強大的功能:線程池具備可拓展性,允許開發人員向其中增加更多的功能。比如延時定時線程池ScheduledThreadPoolExecutor,就允許任務延期執行或定期執行。

同時阿裡巴巴在其《Java開發手冊》中也強制規定:線程資源必須通過線程池提供,不允許在應用中自行顯式建立線程。

說明:線程池的好處是減少在建立和銷毀線程上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統建立大量同類線程而導緻消耗完記憶體或者“過度切換”的問題。

知道了什麼是線程池以及為什要用線程池之後,我們再來看怎麼用線程池。

線程池使用

線程池的建立方法總共有 7 種,但總體來說可分為 2 類:

  • 一類是通過 ThreadPoolExecutor 建立的線程池;
  • 另一個類是通過 Executors 建立的線程池。
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

線程池的建立方式總共包含以下 7 種(其中 6 種是通過 Executors 建立的,1 種是通過ThreadPoolExecutor 建立的):

  1. Executors.newFixedThreadPool:建立一個固定大小的線程池,可控制并發的線程數,超出的線程會在隊列中等待;
  2. Executors.newCachedThreadPool:建立一個可緩存的線程池,若線程數超過處理所需,緩存一段時間後會回收,若線程數不夠,則建立線程;
  3. Executors.newSingleThreadExecutor:建立單個線程數的線程池,它可以保證先進先出的執行順序;
  4. Executors.newScheduledThreadPool:建立一個可以執行延遲任務的線程池;
  5. Executors.newSingleThreadScheduledExecutor:建立一個單線程的可以執行延遲任務的線程池;
  6. Executors.newWorkStealingPool:建立一個搶占式執行的線程池(任務執行順序不确定)【JDK 1.8 添加】。
  7. ThreadPoolExecutor:最原始的建立線程池的方式,它包含了 7 個參數可供設定,後面會詳細講。

單線程池的意義從以上代碼可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 建立的都是單線程池,那麼單線程池的意義是什麼呢?答:雖然是單線程池,但提供了工作隊列,生命周期管理,工作線程維護等功能。

那接下來我們來看每種線程池建立的具體使用。

1.FixedThreadPool

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

使用示例如下:

public static void fixedThreadPool() {
    // 建立 2 個資料級的線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 建立任務
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
        }
    };

    // 線程池執行任務(一次添加 4 個任務)
    // 執行任務的方法有兩種:submit 和 execute
    threadPool.submit(runnable);  // 執行方式 1:submit
    threadPool.execute(runnable); // 執行方式 2:execute
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}
           

執行結果如下:

工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

如果覺得以上方法比較繁瑣,還用更簡單的使用方法,如下代碼所示:

public static void fixedThreadPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    // 執行任務
    threadPool.execute(() -> {
        System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
    });
}
           

2.CachedThreadPool

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

public static void cachedThreadPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newCachedThreadPool();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

從上述結果可以看出,線程池建立了 10 個線程來執行相應的任務。

3.SingleThreadExecutor

建立單個線程數的線程池,它可以保證先進先出的執行順序。

public static void singleThreadExecutor() {
    // 建立線程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + ":任務被執行");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

4.ScheduledThreadPool

建立一個可以執行延遲任務的線程池。

public static void scheduledThreadPool() {
    // 建立線程池
    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
    // 添加定時執行任務(1s 後執行)
    System.out.println("添加任務,時間:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任務被執行,時間:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 1, TimeUnit.SECONDS);
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

從上述結果可以看出,任務在 1 秒之後被執行了,符合我們的預期。

5.SingleThreadScheduledExecutor

建立一個單線程的可以執行延遲任務的線程池。

public static void SingleThreadScheduledExecutor() {
    // 建立線程池
    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
    // 添加定時執行任務(2s 後執行)
    System.out.println("添加任務,時間:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任務被執行,時間:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 2, TimeUnit.SECONDS);
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

從上述結果可以看出,任務在 2 秒之後被執行了,符合我們的預期。

6.newWorkStealingPool

建立一個搶占式執行的線程池(任務執行順序不确定),注意此方法隻有在 JDK 1.8+ 版本中才能使用。

public static void workStealingPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newWorkStealingPool();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被執行,線程名:" + Thread.currentThread().getName());
        });
    }
    // 確定任務執行完成
    while (!threadPool.isTerminated()) {
    }
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

從上述結果可以看出,任務的執行順序是不确定的,因為它是搶占式執行的。

7.ThreadPoolExecutor

最原始的建立線程池的方式,它包含了 7 個參數可供設定。

public static void myThreadPoolExecutor() {
    // 建立線程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被執行,線程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}
           
工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

ThreadPoolExecutor 參數介紹

ThreadPoolExecutor 最多可以設定 7 個參數,如下代碼所示:

 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:

機關是和參數 3 存活時間配合使用的,合在一起用于設定線程的存活時間 ,參數 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:拒絕并抛出異常。
  • CallerRunsPolicy:使用目前調用的線程來執行此任務。
  • DiscardOldestPolicy:抛棄隊列頭部(最舊)的一個任務,并執行目前任務。
  • DiscardPolicy:忽略并抛棄目前任務。

預設政策為 AbortPolicy。

線程池的執行流程

ThreadPoolExecutor 關鍵節點的執行流程如下:

  • 當線程數小于核心線程數時,建立線程。
  • 當線程數大于等于核心線程數,且任務隊列未滿時,将任務放入任務隊列。
  • 當線程數大于等于核心線程數,且任務隊列已滿:若線程數小于最大線程數,建立線程;若線程數等于最大線程數,抛出異常,拒絕任務。

線程池的執行流程如下圖所示:

工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

線程拒絕政策

我們來示範一下 ThreadPoolExecutor 的拒絕政策的觸發,我們使用 DiscardPolicy 的拒絕政策,它會忽略并抛棄目前任務的政策,實作代碼如下:

public static void main(String[] args) {
    // 任務的具體方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("目前任務被執行,執行時間:" + new Date() +
                               " 執行線程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 建立線程,線程的任務隊列的長度為 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
                                                           new ThreadPoolExecutor.DiscardPolicy());
    // 添加并執行 4 個任務
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}
           

我們建立了一個核心線程數和最大線程數都為 1 的線程池,并且給線程池的任務隊列設定為 1,這樣當我們有 2 個以上的任務時就會觸發拒絕政策,執行的結果如下圖所示:

工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

從上述結果可以看出隻有兩個任務被正确執行了,其他多餘的任務就被舍棄并忽略了。其他拒絕政策的使用類似,這裡就不一一贅述了。

自定義拒絕政策

除了 Java 自身提供的 4 種拒絕政策之外,我們也可以自定義拒絕政策,示例代碼如下:

public static void main(String[] args) {
    // 任務的具體方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("目前任務被執行,執行時間:" + new Date() +
                               " 執行線程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 建立線程,線程的任務隊列的長度為 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
                                                           new RejectedExecutionHandler() {
                                                               @Override
                                                               public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                                                   // 執行自定義拒絕政策的相關操作
                                                                   System.out.println("我是自定義拒絕政策~");
                                                               }
                                                           });
    // 添加并執行 4 個任務
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}
           

程式的執行結果如下:

工作這麼多年!很多人竟然不知道線程池的建立方式有7種?

究竟選用哪種線程池?

經過以上的學習我們對整個線程池也有了一定的認識了,那究竟該如何選擇線程池呢?