線程池你真不來了解一下嗎?
前言
只有光頭才能變強
回顧前面:
- ThreadLocal就是這麽簡單
- 多線程三分鐘就可以入個門了!
- 多線程基礎必要知識點!看了學習多線程事半功倍
- Java鎖機制了解一下
- AQS簡簡單單過一遍
- Lock鎖子類了解一下
本篇主要是講解線程池,這是我在多線程的倒數第二篇了,後面還會有一篇死鎖。主要將多線程的基礎過一遍,以後有機會再繼續深入!
那麽接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~
聲明:本文使用JDK1.8
一、線程池簡介
線程池可以看做是線程的集合。在沒有任務時線程處於空閑狀態,當請求到來:線程池給這個請求分配一個空閑的線程,任務完成後回到線程池中等待下次任務(而不是銷毀)
我們來看看如果沒有使用線程池的情況是這樣的:
- 為每個請求都新開一個線程!
public class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
// 為每個請求都創建一個新的線程
final Socket connection = socket.accept ();
Runnable task = () -> handleRequest(connection);
new Thread(task).start();
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
為每個請求都開一個新的線程雖然理論上是可以的,但是會有缺點:
- 線程生命周期的開銷非常高。每個線程都有自己的生命周期,創建和銷毀線程所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,並且還會有某些空閑線程也會占用資源
- 程序的穩定性和健壯性會下降,每個請求開一個線程。如果受到了惡意攻擊或者請求過多(內存不足),程序很容易就奔潰掉了。
所以說:我們的線程最好是交由線程池來管理,這樣可以減少對線程生命周期的管理,一定程度上提高性能。
二、JDK提供的線程池API
JDK給我們提供了Excutor框架來使用線程池,它是線程池的基礎。
- Executor提供了一種將“任務提交”與“任務執行”分離開來的機制(解耦)
下面我們來看看JDK線程池的總體api架構:
接下來我們把這些API都過一遍看看:
Executor接口:
ExcutorService接口:
AbstractExecutorService類:
ScheduledExecutorService接口:
ThreadPoolExecutor類:
ScheduledThreadPoolExecutor類:
2.1ForkJoinPool線程池
除了ScheduledThreadPoolExecutor和ThreadPoolExecutor類線程池以外,還有一個是JDK1.7新增的線程池:ForkJoinPool線程池
於是我們的類圖就可以變得完整一些:
JDK1.7中新增的一個線程池,與ThreadPoolExecutor一樣,同樣繼承了AbstractExecutorService。ForkJoinPool是Fork/Join框架的兩大核心類之一。與其它類型的ExecutorService相比,其主要的不同在於采用了工作竊取算法(work-stealing):所有池中線程會嘗試找到並執行已被提交到池中的或由其他線程創建的任務。這樣很少有線程會處於空閑狀態,非常高效。這使得能夠有效地處理以下情景:大多數由任務產生大量子任務的情況;從外部客戶端大量提交小任務到池中的情況。
來源:
- https://blog.csdn.net/panweiwei1994/article/details/78969238
2.2補充:Callable和Future
學到了線程池,我們可以很容易地發現:很多的API都有Callable和Future這麽兩個東西。
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
其實它們也不是什麽高深的東西~~~
我們可以簡單認為:Callable就是Runnable的擴展。
- Runnable沒有返回值,不能拋出受檢查的異常,而Callable可以!
也就是說:當我們的任務需要返回值的時,我們就可以使用Callable!
Future一般我們認為是Callable的返回值,但他其實代表的是任務的生命周期(當然了,它是能獲取得到Callable的返回值的)
簡單來看一下他們的用法:
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 創建線程池對象
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以執行Runnable對象或者Callable對象代表的線程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));
// V get()
Integer i1 = f1.get();
Integer i2 = f2.get();
System.out.println(i1);
System.out.println(i2);
// 結束
pool.shutdown();
}
}
Callable任務:
public class MyCallable implements Callable<Integer> {
private int number;
public MyCallable(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int x = 1; x <= number; x++) {
sum += x;
}
return sum;
}
}
執行完任務之後可以獲取得到任務返回的數據:
三、ThreadPoolExecutor詳解
這是用得最多的線程池,所以本文會重點講解它。
我們來看看頂部註釋:
3.1內部狀態
變量ctl定義為AtomicInteger,記錄了“線程池中的任務數量”和“線程池的狀態”兩個信息。
線程的狀態:
- RUNNING:線程池能夠接受新任務,以及對新添加的任務進行處理。
- SHUTDOWN:線程池不可以接受新任務,但是可以對已添加的任務進行處理。
- STOP:線程池不接收新任務,不處理已添加的任務,並且會中斷正在處理的任務。
- TIDYING:當所有的任務已終止,ctl記錄的"任務數量"為0,線程池會變為TIDYING狀態。當線程池變為TIDYING狀態時,會執行鉤子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變為TIDYING時,進行相應的處理;可以通過重載terminated()函數來實現。
- TERMINATED:線程池徹底終止的狀態。
各個狀態之間轉換:
3.2已默認實現的池
下面我就列舉三個比較常見的實現池:
- newFixedThreadPool
- newCachedThreadPool
- SingleThreadExecutor
如果讀懂了上面對應的策略呀,線程數量這些,應該就不會太難看懂了。
3.2.1newFixedThreadPool
一個固定線程數的線程池,它將返回一個corePoolSize和maximumPoolSize相等的線程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3.2.2newCachedThreadPool
非常有彈性的線程池,對於新的任務,如果此時線程池裏沒有空閑線程,線程池會毫不猶豫的創建一條新的線程去處理這個任務。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
3.2.3SingleThreadExecutor
使用單個worker線程的Executor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3.3構造方法
我們讀完上面的默認實現池還有對應的屬性,再回到構造方法看看
- 構造方法可以讓我們自定義(擴展)線程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- 指定核心線程數量
- 指定最大線程數量
- 允許線程空閑時間
- 時間對象
- 阻塞隊列
- 線程工廠
- 任務拒絕策略
再總結一遍這些參數的要點:
線程數量要點:
- 如果運行線程的數量少於核心線程數量,則創建新的線程處理請求
- 如果運行線程的數量大於核心線程數量,小於最大線程數量,則當隊列滿的時候才創建新的線程
- 如果核心線程數量等於最大線程數量,那麽將創建固定大小的連接池
- 如果設置了最大線程數量為無窮,那麽允許線程池適合任意的並發數量
線程空閑時間要點:
- 當前線程數大於核心線程數,如果空閑時間已經超過了,那該線程會銷毀。
排隊策略要點:
- 同步移交:不會放到隊列中,而是等待線程執行它。如果當前線程沒有執行,很可能會新開一個線程執行。
- 無界限策略:如果核心線程都在工作,該線程會放到隊列中。所以線程數不會超過核心線程數
- 有界限策略:可以避免資源耗盡,但是一定程度上減低了吞吐量
當線程關閉或者線程數量滿了和隊列飽和了,就有拒絕任務的情況了:
拒絕任務策略:
- 直接拋出異常
- 使用調用者的線程來處理
- 直接丟掉這個任務
- 丟掉最老的任務
四、execute執行方法
execute執行方法分了三步,以註釋的方式寫在代碼上了~
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果線程池中運行的線程數量<corePoolSize,則創建新線程來處理請求,即使其他輔助線程是空閑的。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果線程池中運行的線程數量>=corePoolSize,且線程池處於RUNNING狀態,且把提交的任務成功放入阻塞隊列中,就再次檢查線程池的狀態,
// 1.如果線程池不是RUNNING狀態,且成功從阻塞隊列中刪除任務,則該任務由當前 RejectedExecutionHandler 處理。
// 2.否則如果線程池中運行的線程數量為0,則通過addWorker(null, false)嘗試新建一個線程,新建線程對應的任務為null。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果以上兩種case不成立,即沒能將任務成功放入阻塞隊列中,且addWoker新建線程失敗,則該任務由當前 RejectedExecutionHandler 處理。
else if (!addWorker(command, false))
reject(command);
}
五、線程池關閉
ThreadPoolExecutor提供了shutdown()
和shutdownNow()
兩個方法來關閉線程池
shutdown() :
shutdownNow():
區別:
- 調用shutdown()後,線程池狀態立刻變為SHUTDOWN,而調用shutdownNow(),線程池狀態立刻變為STOP。
- shutdown()等待任務執行完才中斷線程,而shutdownNow()不等任務執行完就中斷了線程。
六、總結
本篇博文主要簡單地將多線程的結構體系過了一篇,講了最常用的ThreadPoolExecutor線程池是怎麽使用的~~~
明天希望可以把死鎖寫出來,敬請期待~~~
還有剩下的幾個線程池(給出了參考資料):
- ScheduledThreadPoolExecutor
- https://blog.csdn.net/panweiwei1994/article/details/78997029
- http://cmsblogs.com/?p=2451
- ForkJoinPool
- https://blog.csdn.net/panweiwei1994/article/details/78992098
參考資料:
- 《Java核心技術卷一》
- 《Java並發編程實戰》
- http://cmsblogs.com/?page_id=111
- https://blog.csdn.net/panweiwei1994/article/details/78483167
- https://zhuanlan.zhihu.com/p/35382932
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關註微信公眾號:Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支持了!希望能多介紹給其他有需要的朋友
文章的目錄導航:
- https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang
線程池你真不來了解一下嗎?