作者主頁:paper jie_博客
本文作者:大家好,我是paper jie,感謝你閱讀本文,歡迎一建三連哦。
本文于《JavaEE》專欄,本專欄是針對于大學生,編程小白精心打造的。筆者用重金(時間和精力)打造,將MySQL基礎知識一網打盡,希望可以幫到讀者們哦。
其他專欄:《MySQL》《C語言》《javaSE》《數據結構》等
內容分享:本期將會分享線程池知識.
目錄
引入
什么是線程池
為什么使用線程池會更高效
Java標準庫中的線程池
ThreadPoolExecutor
?Executors
創建線程池時,怎么設定線程數合適?
實現一個自定義的線程池
具體代碼
引入
在開始,為了解決并發編程的問題,我們引入了進程. 但隨著不斷發展我們發現進程的創建銷毀的開銷會比較大,因此我們就又引入了進程.但是隨著線程創建的頻率提高,這樣的開銷又逐漸大起來了.這個時候我們就有了兩種解決方法.第一種就是引入我們的纖程/攜程. 纖程的本質上就是在用戶態代碼中調度和控制,這樣就可以不讓內核態來調度.這樣就節省了調度的開銷.纖程就是基于線程封裝出來了,一般都是多個纖程對應一個線程.
第二個方法就是我們本文需要講的線程池~
什么是線程池
在我學變成代碼中,我們會經常遇到一些帶有池的名詞,比如: 常量池,數據庫連接池,線程池等. 池的作用就是提前將對象創建好,在需要使用的時候就去池子里面拿,用完了也不要立刻銷毀,而是再放回池里等待下次使用.這樣就可以很好的提高效率,因為節省了創建對象和銷毀對象的開銷.
而線程池也是這樣,它的本質也就是提前將線程在線程池中創建好,使用完后也不會立刻銷毀,會返回線程池等待下一次的使用.這樣也就是節省了創建和調度的開銷.
為什么使用線程池會更高效
因為從線程池中拿線程是在用戶態代碼中調度執行的,是可控的.而直接創建線程是需要在內核態中創建,這時不可控的.因為你也不知道什么時候CPU才會給創建線程.
舉個栗子:
這就像你去銀行辦理銀行卡,它可能需要你打印身份證復印件.這時你有兩個選擇: 1. 直接拿著身份證去大廳的自助打印機打印. 2.把身份證交給工作人員,由她幫你打印. 要是你自己打印就是直接去中途不會干其他的事情,但是交給工作人員保不準她可能沒有第一時間給你打印,而是先去干其他的事情. 這里辦公區就屬于內核態,大廳就屬于用戶態.?
?
Java標準庫中的線程池
ThreadPoolExecutor
Java標準庫中的線程池是ThreadPoolExecutor. 它有多個參數,我們需要去了解一下.我們可以去Java的官方文檔里面找?ThreadPoolExecutor (Java 平臺 SE 8) (oracle.com)?我們需要找到concurrent這個包.這個包里就有ThreadPoolExecutor這個類. 我們可以找到它的構造方法.
這里我們理解第4個即可,其他幾個的參數第4個都包含.
int?corePoolSize: 核心線程數.可以理解為公司里的正式員工.
int?maximumPoolSize: 最大線程數.可以理解為公司里的臨時工,公司不忙了就可以開除的那種.
long?keepAliveTime: 存活時間,就是除去核心線程的其他線程的存活時間. 可以理解為當零時工閑下來多久會被開除.
TimeUnit?unit: 存活時間的單位.
BlockingQueue<Runnable>?workQueue: 阻塞隊列,這個隊列里面存放的就是線程需要執行的任務,線程會到里面去取任務.
ThreadFactory?threadFactory: 線程工廠,線程池就是通過這個工廠類來創建線程. 本質上就是將創建線程對象的操作封裝起來且再設置一些線程的屬性.
RejectedExecutionHandler?handler: 拒絕策略. 當阻塞隊列滿了之后,再添加新的任務進來,這個拒絕策略就會出來處理. 它提供了4個方法:
1. 直接拋出異常,新的任務和舊的任務都不執行了.
2. 新的任務由這個添加新任務的線程來執行.
3. 丟棄最舊的任務,再將這個新的任務添加進阻塞隊列.
4. 丟棄這個需要添加進來的新任務,繼續照常執行.
?Executors
因為這個類的參數比較多,用起來比較復雜.Java就又用一個類將它封裝起來了,變成了一個比較簡單的線程類Executors.它也是一個工廠類. 也是通過這個類創建好不同的線程池對象,在它的內部就已經創建好了線程池對象且設置了一些它的參數.
它有好幾種創建線程池的方式:
newFixedThreadPool | 創建固定線程數的線程池 |
newCachedThreadPool | 創建線程數可以動態增長的線程池 |
newSingleThreadExecutor | 創建只含有單個線程的線程池 |
newScheduledThreadPool | 創建線程可以延時執行任務的線程池,類似于定時器 |
這里兩個類我們可以看情況來使用.
創建線程池時,怎么設定線程數合適?
這里用一句話來概括就是具體問題具體分析.
我們線程執行任務分為兩種: 一種為CPU密集型,一種為IO密集型.CPU密集型的線程就是使用CPU的時間比較長.而IO密集型的線程就是使用CPU時間少,大多數時間都是在IO等待中. 這里極端一點,都是CPU密集型的話線程池的線程數不能超過邏輯核心數,而都是IO密集型的話線程數就可以遠遠超過邏輯核心數了.
但是在我們實際開發中,一般都是CPU密集型一部分,IO密集型一部分.這種情況下我們就需要具體問題具體分析了.最好的辦法就是進行性能測試,給線程池的線程數進行多組不同數目的測試,觀察他們的系統資源開銷和時間開銷,取其中最好的即可.
實現一個自定義的線程池
這里我們實現一個簡單的固定線程數的線程池.
我們需要:
1. 一個存放任務的阻塞隊列
2. 一個核心方法來添加任務.
3. 用構造方法來指定線程數,創建好線程.
具體代碼
class MyThreadPoolExecutor3 {//1. 存放任務的阻塞隊列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);//2. 構造方法public MyThreadPoolExecutor3(int capacity) {for(int i = 0; i <capacity; i++) {Thread t = new Thread(() -> {while(true) {Runnable runnable = null;try {runnable = queue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}runnable.run();}});t.start();}}//核心方法 submit 添加方法public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}
}
public class ThreadDemo3 {public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor3 myThreadPoolExecutor3 = new MyThreadPoolExecutor3(4);for (int i = 0; i < 1000; i++) {int n = i;myThreadPoolExecutor3.submit(() -> {System.out.println(n + " " + Thread.currentThread().getName() + " hello");});}}}