-
為什么要用線程池?
我們都知道,每一次創建一個線程,JVM后面的工作包括:為線程建立虛擬機棧、本地方法棧、程序計數器的內存空間(下圖可看出),所以線程過多容易導致內存空間溢出。同時,當頻繁的創建和銷毀線程容易浪費系統的計算能力在資源的回收和申請中。
另外:創建過多的線程,會導致cpu在線程中的切換時間比處理時間還多,大大降低了系統的吞吐量。因此我們使用線程池如下好處:
- 有效控制線程的數量,防止線程數量過多。
- 提高線程的利用程度,避免頻繁的創建及銷毀線程。
- 有更靈活的線程使用方式及拒絕措施。
再給大家看看阿里開發規約里面是怎么說的
-
線程的快速示例
我知道大多數人都希望先看看線程池怎么創建,然后再深入了解。下面給大家一個demo

1 //存放任務的阻塞隊列 2 BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>(10); 3 //BasicThreadFactory是自己實現ThreadFactory接口而來 4 BasicThreadFactory factory = new BasicThreadFactory(); 5 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 10, 60, 6 TimeUnit.SECONDS, queue, factory, 7 (Runnable r, ThreadPoolExecutor executor)->{ 8 System.out.println(executor.getQueue().size()+"消息隊列已滿"); 9 System.out.println("拒絕服務"); 10 11 });
-
線程池相關概念
- 核心線程:若線程池中的線程標記為核心線程,即使核心線程沒有運行任務,它也不會被銷毀,會一直存在於線程池中,直至線程池被shutdown。
- 非核心線程:當線程池中沒有空閑的核心線程時,線程池會創建一個非核心線程,並且非核心線程的一定時間內處於空閑狀態的時候,非核心線程會被銷毀。
- 阻塞隊列:阻塞隊列是當線程池中的沒有能用於處理任務的線程時,會把該任務放入阻塞隊列,待有能用於處理的線程時,把任務從隊列取出處理,阻塞隊列的長度可以設置。
- 拒絕服務處理:當線程池中的沒有線程能提供處理,並且阻塞隊列的空間已滿,此時會觸發拒絕服務異常,開發人員可以根據自己的需求定制不同的處理策略。
-
創建線程池的7個參數
一般我們推薦使用ThreadPoolExecutor()自定義創建線程池,因為這比較靈活切可控。
-
int corePoolSize 核心線程數,即確定有多少個核心線程。
-
int maximumPoolSize 最大線程數,即限定線程池中的最大線程數量。
-
long keepAliveTime 非核心線程的存活時間,配合下面的TimeUnit參數確定時間。
-
TimeUnit unit 一個時間類型的枚舉類。有從納秒到天的時間量度,配合上面的keepAliveTime確定非核心線程的存活時間。
-
BlockingQueue<Runnable> workQueue 裝載Runnable的阻塞隊列,具體類型可以自己確定。
-
ThreadFactory threadFactory 線程工廠,這是一個函數式接口,里面只定義了一個newThread(Runnable task)方法,需要自己實現工廠的方法,在這里我們可以對線程進行自定義的初始化,例如給線程設定名字,這樣方便后期的調試。
-
RejectedExecutionHandler handler 拒絕服務處理,這也是一個函數式接口,我們需要實現rejectedExecution(Runnable r, ThreadPoolExecutor executor)這個方法,這里可以根據需求自定義你希望在處理邏輯。當然Java里面也有已經定義好的四種策略靜態類。可以通過ThreadPoolExecutor調用
-
Executors中實現的線程池類型
下面介紹的線程池類型,是Jdk幫我們制定好的策略。但是,有的線程池類型中,要么存在線程數量無限制、要么存在阻塞隊列長度無限制,但是這些應該在開發中避免,因為一旦並發過高,會導致大量的對象積壓,導致JVM內存溢出。
寫在前面:jdk提供了默認的工廠方法和默認的默認的拒絕處理策略。
默認拒絕策略是:不執行並拋出異常
默認的工廠方法是:對線程進行安全檢查並命名。

1 static class DefaultThreadFactory implements ThreadFactory { 2 private static final AtomicInteger poolNumber = new AtomicInteger(1); 3 private final ThreadGroup group; 4 private final AtomicInteger threadNumber = new AtomicInteger(1); 5 private final String namePrefix; 6 7 DefaultThreadFactory() { 8 SecurityManager s = System.getSecurityManager(); 9 group = (s != null) ? s.getThreadGroup() : 10 Thread.currentThread().getThreadGroup(); 11 namePrefix = "pool-" + 12 poolNumber.getAndIncrement() + 13 "-thread-"; 14 } 15 16 public Thread newThread(Runnable r) { 17 Thread t = new Thread(group, r, 18 namePrefix + threadNumber.getAndIncrement(), 19 0); 20 if (t.isDaemon()) 21 t.setDaemon(false); 22 if (t.getPriority() != Thread.NORM_PRIORITY) 23 t.setPriority(Thread.NORM_PRIORITY); 24 return t; 25 } 26 }
- FixedThreadPool 固定核心線程的線程池。
特點:它的核心線程數量就是最大線程數,所以線程池內的線程永遠不會消亡,它采用了無參數的鏈表阻塞隊列,最大的任務數可達232-1個。因此存在任務積壓導致內存溢出的風險。
2. CachedThreadPool 緩存線程池
特點:沒有核心線程,線程池不能滿足任務運行時會創建新的線程,線程數量沒有上限。默認的消亡時間為60秒。值得注意的是:它的阻塞隊列是SynchronousQueue,這是一個沒有存儲性質的阻塞隊列,它的取值操作和放入操作必須是互斥的。根據源碼文檔的解釋,可以理解為每當有任務放入時會立即有線程將它取出執行。
3. ScheduledThreadPool 固定調度線程池
特點:有固定的核心線程,線程的數量沒有限制,默認存活時間為60秒。同時支持定時及周期性任務執行。
4. SingleThreadExecutor 單核心線程池
特點:只有一個核心線程,所以能保證任務的串行化執行。
5. WorkStealingPool 並行執行線程池
特點:在jdk8中實現 線程池。它內部的線程池實現是ForkJoinPool,這是一個可以同時利用多個線程來執行任務的線程池。無參默認使用CPU數量的線程數執行任務,由於這個線程池比較復雜,下次專門寫一篇博文用於更新。
-
線程池的調用流程
需要注意的是:線程池設計的流程是先利用核心線程處理、核心線程不能處理即把它放入阻塞隊列,最好才創建線程來執行任務,直到新建線程也失敗才調用拒絕服務處理。
試着理解一下這樣設計的好處。可以看到,創建線程永遠不是最先想到的辦法,線程池盡量避免創建線程。因為創建線程需要調用全局鎖來確定線程的正確創建,同時也因為線程創建和銷毀也需要消耗資源,所以這種方式在最大努力的避免這種情況的發生。
-
線程池的關閉
雖然在實際的開發中,線程池一般是隨着項目的部署一起存活的,不會經常關閉,但是還是需要了解如何關閉,怎么關閉比較安全。
線程池可通過調用線程池的shutdown或shutdownNow方法來關閉線程池.
它們的原理是遍歷線程池中的工作線程,然后逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止.
但是它們存在一定的區別
- shutdownNow首先將線程池的狀態設置成STOP,然后嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表
- shutdown只是將線程池的狀態設置成SHUTDOWN狀態,然后中斷所有沒有正在執行任務的線程.
只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true.
當所有的任務都已關閉后,才表示線程池關閉成功,這時調用isTerminaed方法會返回true.
至於應該調用哪一種方法,應該由提交到線程池的任務的特性決定,通常調用shutdown方法來關閉線程池,若任務不一定要執行完,則可以調用shutdownNow方法.
線程關閉的方法轉載於作者:全網搜索關注JavaEdge
鏈接:https://www.nowcoder.com/discuss/152050?type=0&order=0&pos=6&page=0