執行緒池
1. Callable介面的使用
package com.yuxue.juc.threadPool;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 多執行緒中,第三種獲得多執行緒的方式
* */
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//FutureTask(Callable<V> callable)
FutureTask<Integer> futureTask = new FutureTask<>(new myThread());
new Thread(futureTask, "AAA").start();
//new Thread(futureTask, "BBB").start();//複用,直接取值,不要重啟兩個執行緒
int a = 100;
int b = 0;
//b = futureTask.get();//要求獲得Callable執行緒的計算結果,如果沒有計算完成就要去強求,會導致堵塞,直到計算完成
while (!futureTask.isDone()) {
////當futureTask完成後取值
b = futureTask.get();
}
System.out.println("===Result is:" + (a + b));
}
}
class myThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\tget in the callable");
Thread.sleep(5000);
return 1024;
}
}
兩者區別:
- Callable:有返回值,拋異常
- Runnable:無返回值,不丟擲異常
2. 為什麼要使用執行緒池
-
執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務放入佇列,然後線上程建立後啟動給這些任務,如果執行緒數量超過了最大數量,超出數量的執行緒排隊等候,等其他執行緒執行完畢,再從佇列中取出任務來執行
-
主要特點
執行緒複用、控制最大併發數、管理執行緒
- 減少建立和銷燬執行緒上所花的時間以及系統資源的開銷 => 減少記憶體開銷,建立執行緒佔用記憶體,建立執行緒需要時間,會延遲處理的請求;降低資源消耗,通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗
- 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行
- 提高執行緒的客觀理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控(根據系統承受能力,達到執行的最佳效果) => 避免無限建立執行緒引起的
OutOfMemoryError
【簡稱OOM】
3. 執行緒池如何使用?
-
架構說明
Java中的執行緒池是通過Executor框架實現的,該框架中用到了 :
Executor,Executors,ExecutorService,ThreadPoolExecutor
-
編碼實現
實現有五種,
Executors.newScheduledThreadPool()
是帶時間排程的,java8新推出Executors.newWorkStealingPool(int)
,使用目前機器上可用的處理器作為他的並行級別重點有三種
-
Executors.newFixedThreadPool(int)
執行長期的任務,效能好很多
建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒回在佇列中等待newFixedThreadPool建立的執行緒池corePoolSize和maximumPoolSize值是相等的,它使用的是 LinkedBlockingQueue
底層原始碼:
-
Executors.newSingleThreadExecutor()
一個任務一個任務執行的場景
建立一個單執行緒話的執行緒池,他只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序執行
newSingleThreadExecutor將corePoolSize和maximumPoolSize都設定為1,使用LinkedBlockingQueue
原始碼:
-
Executors.newCachedThreadPool()
執行很多短期非同步的小程式或負載較輕的伺服器
建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒縣城,若無可回收,則新建執行緒
newCachedThreadPool將corePoolSize設定為0,將maximumPoolSize設定為Integer.MAX_VALUE,使用 的SynchronousQueue,也就是說來了任務就建立執行緒執行,當縣城空閒超過60s,就銷燬執行緒
原始碼:
我們可以看到底層的程式碼都是由ThreadPoolExecutor這個類的構造方法建立的,只是傳入的引數不同,那麼研究一下這個類以及這些引數就很有必要,下節我們將介紹這些引數的使用
-
-
ThreadPoolExecutor
4. 執行緒池的幾個重要引數
核心執行程式碼為:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
那麼我們再點進this方法可以看到其全引數的構造方法為:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
簡單介紹一下:
-
corePoolSize: 執行緒池中常駐核心執行緒數
- 在建立了執行緒池後,當有請求任務來之後,就會安排池中的執行緒去執行請求任務
- 當執行緒池的執行緒數達到corePoolSize後,就會把到達的任務放到快取佇列當中
-
maximumPoolSize: 執行緒池能夠容納同時執行的最大執行緒數,必須大於等於1
-
keepAliveTime: 多餘的空閒執行緒的存活時間
- 當前執行緒池數量超過corePoolSize時,檔口空閒時間達到keepAliveTime值時,多餘空閒執行緒會被銷燬到只剩下corePoolSize個執行緒為止
-
unit: keepAliveTime的單位
-
workQueue: 任務佇列,被提交但尚未被執行的任務
任務佇列底層是BlockingQueue阻塞佇列!不清楚阻塞佇列的參考這篇文章:用阻塞佇列實現一個生產者消費者模型?
-
threadFactory:表示生成執行緒池中工作執行緒的執行緒工廠,用於建立執行緒一般用預設的即可
-
handler: 拒絕策略,表示當佇列滿了並且工作執行緒大於等於執行緒池的最大執行緒數(maximumPoolSize)時如 何來拒絕請求執行的runable的策略
5. 執行緒池的底層工作原理以及過程
如上圖所屬,其流程為:
-
在建立了執行緒池之後,等待提交過來的任務請求
-
當呼叫execute()方法新增一個請求任務時,執行緒池會做出如下判斷:
2.1 如果正在執行的執行緒數量小於corePoolSize,那麼馬上建立執行緒執行這個任務;
2.2 如果正在執行的執行緒數量大於或等於corePoolSize,那麼將這個任務放入佇列;
2.3 如果此時佇列滿了且執行的執行緒數小於maximumPoolSize,那麼還是要建立非核心執行緒立刻執行此任務;
2.4 如果佇列滿了且正在執行的執行緒數量大於或等於maxmumPoolSize,那麼啟動飽和拒絕策略來執行
-
當一個執行緒完成任務時,他會從佇列中取出下一個任務來執行
-
當一個執行緒無事可做超過一定的時間(keepAliveTime)時,執行緒池會判斷:
如果當前執行的執行緒數大於corePoolSize,那麼這個執行緒會被停掉;所以執行緒池的所有任務完成後他最大會收 縮到corePoolSize的大小
5. 實際工作中如何設定合理引數
5.1 執行緒池的拒絕策略
- 什麼是執行緒的拒絕策略
- 等待佇列也已經排滿了,再也塞不下新任務了,同時執行緒池中的max執行緒也達到了,無法繼續為新任務服務。這時我們就需要拒絕策略機制合理的處理這個問題
- JDK內建的拒絕策略
- AbortPolicy(預設):如果滿了會直接丟擲RejectedExecutionException異常阻止系統正常執行
- CallerRunsPolicy:”呼叫者執行“一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫者, 從而降低新任務的流量
- DiscardOldestPolicy:拋棄佇列中等待最久的任務,然後把當前任務加入佇列中嘗試再次提交當前任務
- DiscardPolicy:直接丟棄任務,不予任何處理也不拋異常。如果允許任務丟失,這是最好的一種方案
- 均實現了RejectedExecutionHandler介面
5.2 你在工作中單一的/固定數的/可變的三種建立執行緒池的方法,用哪個多?
回答:一個都不用,我們生產上只能使用自定義的!!!!
為什麼?
執行緒池不允許使用Executors建立,試試通過ThreadPoolExecutor的方式,規避資源耗盡風險
阿里巴巴規範手冊當中提到:
- FixedThreadPool和SingleThreadPool允許請求佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,導致OOM;
- CachedThreadPool和ScheduledThreadPool允許的建立執行緒數量為Integer.MAX_VALUE,可能會建立大量執行緒,導致OOM
5.3 你在工作中時如何使用執行緒池的,是否自定義過執行緒池?
上程式碼:
package com.yuxue.juc.threadPool;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
//corePoolSize:常駐核心執行緒數
2,
//maximumPoolSize:最大的可容納執行緒數
5,
//存活時間設定為1s
1L,
TimeUnit.SECONDS,
//這裡用LinkedBlockingQueue,且容量為3,意味著等候區最大容量三個任務
new LinkedBlockingQueue<>(3),
//預設的defaultThreadFactory即可
Executors.defaultThreadFactory(),
//丟棄方法使用AbortPolicy()
new ThreadPoolExecutor.AbortPolicy());
//這裡用來做任務的處理執行
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 辦理業務;");
});
}
threadPoolExecutor.shutdown();
}
}
我們執行結果為:
pool-1-thread-2 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
//可以看到當我們任務書為5且處理速度非常快時,我們就用核心的corePoolSize就可以滿足任務需求
當任務數量變多或者任務變重時:如將我們的任務數量調整為20時,此時執行結果為:
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-4 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-5 辦理業務;
pool-1-thread-4 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.yuxue.juc.threadPool.MyThreadPoolDemo$$Lambda$1/558638686@6d03e736 rejected from java.util.concurrent.ThreadPoolExecutor@568db2f2[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 15]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.yuxue.juc.threadPool.MyThreadPoolDemo.main(MyThreadPoolDemo.java:27)
發生了異常,且任務只執行完了15個,我們可以看到其中active threads = 0, queued tasks = 0也就是說我的阻塞佇列已經滿了,且沒有空閒的執行緒了,此時再申請任務我就會丟擲異常,這是執行緒池handler引數的拒絕策略,當我們更改策略為ThreadPoolExecutor.CallerRunsPolicy()
時,執行結果當中存在main 辦理業務;
語句,也就意味著執行緒池將某些任務回退到了呼叫者,另外的兩個拒絕策略在此就不演示
5.4 合理配置執行緒池你是如何考慮的?
-
CPU密集型:
- CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行
- CPU密集任務只有在真正多核CPU上才可能得到加速(通過多執行緒) 而在單核CPU上,無論你開幾個模擬的多執行緒該任務都不可能得到加速,因為CPU總的運算能力就那些
- CPU密集型任務配置儘可能少的執行緒數量
一般公式:CPU核數+1個執行緒的執行緒池
-
IO密集型
- 由於IO密集型任務執行緒並不是一直在執行任務,則應配置經可能多的執行緒,如CPU核數 * 2
- IO密集型,即該任務需要大量的IO,即大量的阻塞。在單執行緒上執行IO密集型的任務會導致浪費大量的 CPU運算能力浪費在等待。
- 所以在IO密集型任務中使用多執行緒可以大大的加速程式執行,即使在單核CPU上,這種加速主要就是利用 了被浪費掉的阻塞時間。
- IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數:
參考公式:CPU核數/(1-阻塞係數) 阻塞係數在0.8~0.9之間
例如八核CPU:,利用公式,約為:8/(1-0,9)=80個執行緒