您好,登錄后才能下訂單哦!
這篇文章主要講解了“為什么要用線程池”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“為什么要用線程池”吧!
下面是一段創建線程并運行的代碼:
for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println("run thread->" + Thread.currentThread().getName()); userService.updateUser(....); }).start(); }
我們想使用這種方式去做異步,或者提高性能,然后將某些耗時操作放入一個新線程去運行。
這種思路是沒問題的,但是這段代碼是存在問題的,有哪些問題呢?下面我們就來看看有哪些問題;
創建銷毀線程資源消耗;我們使用線程的目的本是出于效率考慮,可以為了創建這些線程卻消耗了額外的時間,資源,對于線程的銷毀同樣需要系統資源。
cpu資源有限,上述代碼創建線程過多,造成有的任務不能即時完成,響應時間過長。
線程無法管理,無節制地創建線程對于有限的資源來說似乎成了“得不償失”的一種作用。
既然我們上面使用手動創建線程會存在問題,那有解決方法嗎?
答案:有的,使用線程池。
線程池介紹
線程池(Thread Pool):把一個或多個線程通過統一的方式進行調度和重復使用的技術,避免了因為線程過多而帶來使用上的開銷。
線程池有什么優點?
降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
提高線程的可管理性。
線程池使用
在JDK中rt.jar包下JUC(java.util.concurrent)創建線程池有兩種方式:ThreadPoolExecutor 和 Executors,其中 Executors又可以創建 6 種不同的線程池類型。
ThreadPoolExecutor 的使用
線程池使用代碼如下:
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolDemo { private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100)); public static void main(String[] args) { threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("田先生您好"); } }); } }
以上程序執行結果如下:
田先生您好
核心參數說明
ThreadPoolExecutor的構造方法有以下四個:
可以看到最后那個構造方法有 7 個構造參數,其實前面的三個構造方法只是對最后那個方法進行包裝,并且前面三個構造方法最終都是調用最后那個構造方法,所以我們這里就來聊聊最后那個構造方法。
參數解釋
corePoolSize
線程池中的核心線程數,默認情況下核心線程一直存活在線程池中,如果將 ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設為 true,如果線程池一直閑置并超過了 keepAliveTime 所指定的時間,核心線程就會被終止。
maximumPoolSize
最大線程數,當線程不夠時能夠創建的最大線程數。
keepAliveTime
線程池的閑置超時時間,默認情況下對非核心線程生效,如果閑置時間超過這個時間,非核心線程就會被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 設為 true 的時候,核心線程如果超過閑置時長也會被回收。
unit
配合 keepAliveTime 使用,用來標識 keepAliveTime 的時間單位。
workQueue
線程池中的任務隊列,使用 execute() 或 submit() 方法提交的任務都會存儲在此隊列中。
threadFactory
為線程池提供創建新線程的線程工廠。
rejectedExecutionHandler
線程池任務隊列超過最大值之后的拒絕策略,RejectedExecutionHandler 是一個接口,里面只有一個 rejectedExecution 方法,可在此方法內添加任務超出最大值的事件處理。ThreadPoolExecutor 也提供了 4 種默認的拒絕策略:
DiscardPolicy():丟棄掉該任務,不進行處理。
DiscardOldestPolicy():丟棄隊列里最近的一個任務,并執行當前任務。
AbortPolicy():直接拋出 RejectedExecutionException 異常(默認)。
CallerRunsPolicy():既不拋棄任務也不拋出異常,直接使用主線程來執行此任務。
包含所有參數的使用案例:
public class ThreadPoolExecutorTest { public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2), new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); threadPool.allowCoreThreadTimeOut(true); for (int i = 0; i < 10; i++) { threadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } } class MyThreadFactory implements ThreadFactory { private AtomicInteger count = new AtomicInteger(0); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); String threadName = "MyThread" + count.addAndGet(1); t.setName(threadName); return t; } }
運行輸出:
main MyThread1 main MyThread1 MyThread1 ....
這里僅僅是為了演示所有參數自定義,并沒有其他用途。
execute() 和 submit()的使用
execute() 和 submit() 都是用來執行線程池的,區別在于 submit() 方法可以接收線程池執行的返回值。
下面分別來看兩個方法的具體使用和區別:
// 創建線程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100)); // execute 使用 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("老田您好"); } }); // submit 使用 Future<String> future = threadPoolExecutor.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("田先生您好"); return "返回值"; } }); System.out.println(future.get());
以上程序執行結果如下:
老田您好 田先生您好 返回值
Executors
Executors 執行器創建線程池很多基本上都是在 ThreadPoolExecutor 構造方法上進行簡單的封裝,特殊場景根據需要自行創建。可以把Executors理解成一個工廠類 。Executors可以創建 6 種不同的線程池類型。
下面對這六個方法進行簡要的說明:
newFixedThreadPool
創建一個數量固定的線程池,超出的任務會在隊列中等待空閑的線程,可用于控制程序的最大并發數。
newCacheThreadPool
短時間內處理大量工作的線程池,會根據任務數量產生對應的線程,并試圖緩存線程以便重復使用,如果限制 60 秒沒被使用,則會被移除緩存。如果現有線程沒有可用的,則創建一個新線程并添加到池中,如果有被使用完但是還沒銷毀的線程,就復用該線程。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。因此,長時間保持空閑的線程池不會使用任何資源。
newScheduledThreadPool
創建一個數量固定的線程池,支持執行定時性或周期性任務。
newWorkStealingPool
Java 8 新增創建線程池的方法,創建時如果不設置任何參數,則以當前機器CPU 處理器數作為線程個數,此線程池會并行處理任務,不能保證執行順序。
newSingleThreadExecutor
創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當于單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
newSingleThreadScheduledExecutor
此線程池就是單線程的 newScheduledThreadPool。
線程池如何關閉?
線程池關閉,可以使用 shutdown() 或 shutdownNow() 方法,它們的區別是:
shutdown():不會立即終止線程池,而是要等所有任務隊列中的任務都執行完后才會終止。執行完 shutdown 方法之后,線程池就不會再接受新任務了。
shutdownNow():執行該方法,線程池的狀態立刻變成 STOP 狀態,并試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,執行此方法會返回未執行的任務。
下面用代碼來模擬 shutdown() 之后,給線程池添加任務,代碼如下:
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class ThreadPoolExecutorAllArgsTest { public static void main(String[] args) throws InterruptedException, ExecutionException { //創建線程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2), new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); threadPoolExecutor.allowCoreThreadTimeOut(true); //提交任務 threadPoolExecutor.execute(() -> { for (int i = 0; i < 3; i++) { System.out.println("提交任務" + i); try { Thread.sleep(3000); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } }); threadPoolExecutor.shutdown(); //再次提及任務 threadPoolExecutor.execute(() -> { System.out.println("我想再次提及任務"); }); } }
以上程序執行結果如下:
提交任務0 提交任務1 提交任務2
可以看出,shutdown() 之后就不會再接受新的任務了,不過之前的任務會被執行完成。
面試題
面試題1:ThreadPoolExecutor 有哪些常用的方法?
ThreadPoolExecutor有如下常用方法:
submit()/execute():執行線程池
shutdown()/shutdownNow():終止線程池
isShutdown():判斷線程是否終止
getActiveCount():正在運行的線程數
getCorePoolSize():獲取核心線程數
getMaximumPoolSize():獲取最大線程數
getQueue():獲取線程池中的任務隊列
allowCoreThreadTimeOut(boolean):設置空閑時是否回收核心線程
這些方法可以用來終止線程池、線程池監控等。
面試題2:說說submit(和 execute兩個方法有什么區別?
submit() 和 execute() 都是用來執行線程池的,只不過使用 execute() 執行線程池不能有返回方法,而使用 submit() 可以使用 Future 接收線程池執行的返回值。
說說線程池創建需要的那幾個核心參數的含義
ThreadPoolExecutor 最多包含以下七個參數:
corePoolSize:線程池中的核心線程數
maximumPoolSize:線程池中最大線程數
keepAliveTime:閑置超時時間
unit:keepAliveTime 超時時間的單位(時/分/秒等)
workQueue:線程池中的任務隊列
threadFactory:為線程池提供創建新線程的線程工廠
rejectedExecutionHandler:線程池任務隊列超過最大值之后的拒絕策略
面試題3:shutdownNow() 和 shutdown() 兩個方法有什么區別?
shutdownNow() 和 shutdown() 都是用來終止線程池的,它們的區別是,使用 shutdown() 程序不會報錯,也不會立即終止線程,它會等待線程池中的緩存任務執行完之后再退出,執行了 shutdown() 之后就不能給線程池添加新任務了;shutdownNow() 會試圖立馬停止任務,如果線程池中還有緩存任務正在執行,則會拋出 java.lang.InterruptedException: sleep interrupted 異常。
面試題6:了解過線程池的工作原理嗎?
當線程池中有任務需要執行時,線程池會判斷如果線程數量沒有超過核心數量就會新建線程池進行任務執行,如果線程池中的線程數量已經超過核心線程數,這時候任務就會被放入任務隊列中排隊等待執行;如果任務隊列超過最大隊列數,并且線程池沒有達到最大線程數,就會新建線程來執行任務;如果超過了最大線程數,就會執行拒絕執行策略。
面試題5:線程池中核心線程數量大小怎么設置?
「CPU密集型任務」:比如像加解密,壓縮、計算等一系列需要大量耗費 CPU 資源的任務,大部分場景下都是純 CPU 計算。盡量使用較小的線程池,一般為CPU核心數+1。因為CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。
「IO密集型任務」:比如像 MySQL 數據庫、文件的讀寫、網絡通信等任務,這類任務不會特別消耗 CPU 資源,但是 IO 操作比較耗時,會占用比較多時間。可以使用稍大的線程池,一般為2*CPU核心數。IO密集型任務CPU使用率并不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。
另外:線程的平均工作時間所占比例越高,就需要越少的線程;線程的平均等待時間所占比例越高,就需要越多的線程;
以上只是理論值,實際項目中建議在本地或者測試環境進行多次調優,找到相對理想的值大小。
面試題7:線程池為什么需要使用(阻塞)隊列?
主要有三點:
因為線程若是無限制的創建,可能會導致內存占用過多而產生OOM,并且會造成cpu過度切換。
創建線程池的消耗較高。
面試題8:線程池為什么要使用阻塞隊列而不使用非阻塞隊列?
阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。
當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。
使得在線程不至于一直占用cpu資源。
(線程執行完任務后通過循環再次從任務隊列中取出任務進行執行,代碼片段如下
while (task != null || (task = getTask()) != null) {})。
不用阻塞隊列也是可以的,不過實現起來比較麻煩而已,有好用的為啥不用呢?
面試題9:了解線程池狀態嗎?
通過獲取線程池狀態,可以判斷線程池是否是運行狀態、可否添加新的任務以及優雅地關閉線程池等。
RUNNING:線程池的初始化狀態,可以添加待執行的任務。
SHUTDOWN:線程池處于待關閉狀態,不接收新任務僅處理已經接收的任務。
STOP:線程池立即關閉,不接收新的任務,放棄緩存隊列中的任務并且中斷正在處理的任務。
TIDYING:線程池自主整理狀態,調用 terminated() 方法進行線程池整理。
TERMINATED:線程池終止狀態。
面試題10:知道線程池中線程復用原理嗎?
線程池將線程和任務進行解耦,線程是線程,任務是任務,擺脫了之前通過 Thread 創建線程時的一個線程必須對應一個任務的限制。
在線程池中,同一個線程可以從阻塞隊列中不斷獲取新任務來執行,其核心原理在于線程池對 Thread 進行了封裝,并不是每次執行任務都會調用 Thread.start() 來創建新線程,而是讓每個線程去執行一個“循環任務”,在這個“循環任務”中不停的檢查是否有任務需要被執行,如果有則直接執行,也就是調用任務中的 run 方法,將 run 方法當成一個普通的方法執行,通過這種方式將只使用固定的線程就將所有任務的 run 方法串聯起來。
感謝各位的閱讀,以上就是“為什么要用線程池”的內容了,經過本文的學習后,相信大家對為什么要用線程池這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。