線程池的使用
上一篇看這裏:JAVA併發編程-7-併發容器
一、爲什麼要使用線程池
1、降低資源的消耗。降低線程創建和銷燬的資源消耗;
2、提高響應速度:線程的創建時間爲T1,執行時間T2,銷燬時間T3,免去T1和T3的時間
3、提高線程的可管理性。
二、手動實現一個線程池
如何實現線程呢?有2個關鍵點
1、線程必須在池子已經創建好了,並且可以保持住,要有容器保存多個線程;
2、線程還要能夠接受外部的任務,運行這個任務。容器保持這個來不及運行的任務.
/**
* 類說明:自己線程池的實現
*/
public class MyThreadPool2 {
// 線程池中默認線程的個數爲5
private static int WORK_NUM = 5;
// 隊列默認任務個數爲100
private static int TASK_COUNT = 100;
// 工作線程組
private WorkThread[] workThreads;
// 任務隊列,作爲一個緩衝
private final BlockingQueue<Runnable> taskQueue;
private final int worker_num;//用戶在構造這個池,希望的啓動的線程數
// 創建具有默認線程個數的線程池
public MyThreadPool2() {
this(WORK_NUM, TASK_COUNT);
}
// 創建線程池,worker_num爲線程池中工/作線程的個數
public MyThreadPool2(int worker_num, int taskCount) {
if (worker_num <= 0) worker_num = WORK_NUM;
if (taskCount <= 0) taskCount = TASK_COUNT;
this.worker_num = worker_num;
taskQueue = new ArrayBlockingQueue<>(taskCount);
workThreads = new WorkThread[worker_num];
for (int i = 0; i < worker_num; i++) {
workThreads[i] = new WorkThread();
workThreads[i].start();
}
}
// 執行任務,其實只是把任務加入任務隊列,什麼時候執行有線程池管理器決定
public void execute(Runnable task) {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 銷燬線程池,該方法保證在所有任務都完成的情況下才銷燬所有線程,否則等待任務完成才銷燬
public void destroy() {
// 工作線程停止工作,且置爲null
System.out.println("ready close pool.....");
for (int i = 0; i < worker_num; i++) {
workThreads[i].stopWorker();
workThreads[i] = null;//help gc
}
taskQueue.clear();// 清空任務隊列
}
// 覆蓋toString方法,返回線程池信息:工作線程個數和已完成任務個數
@Override
public String toString() {
return "WorkThread number:" + worker_num
+ " wait task number:" + taskQueue.size();
}
/**
* 內部類,工作線程
*/
private class WorkThread extends Thread {
@Override
public void run() {
Runnable r = null;
try {
while (!isInterrupted()) {
r = taskQueue.take();
if (r != null) {
System.out.println(getId() + " ready exec :" + r);
r.run();
}
r = null;//help gc;
}
} catch (Exception e) {
// TODO: handle exception
}
}
public void stopWorker() {
interrupt();
}
}
}
上面類中,在構造方法中,定義好線程容器,並且在其中創建指定數量的線程,定義了任務隊列來存放任務。
execute方法就是向任務隊列中放入需要執行的任務。
定義了WorkThread作爲單個工作線程,它不斷的從taskQueue任務隊列中去取得任務,然後去運行它。
public class TestMyThreadPool {
public static void main(String[] args) throws InterruptedException {
// 創建3個線程的線程池
MyThreadPool2 t = new MyThreadPool2(3,0);
t.execute(new MyTask("testA"));
t.execute(new MyTask("testB"));
t.execute(new MyTask("testC"));
t.execute(new MyTask("testD"));
t.execute(new MyTask("testE"));
System.out.println(t);
Thread.sleep(10000);
t.destroy();// 所有線程都執行完成才destory
System.out.println(t);
}
// 任務類
static class MyTask implements Runnable {
private String name;
private Random r = new Random();
public MyTask(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void run() {// 執行任務
try {
Thread.sleep(r.nextInt(1000)+2000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
+Thread.currentThread().isInterrupted());
}
System.out.println("任務 " + name + " 完成");
}
}
}
我們自己實現的線程池還有很多缺點:
1,線程池在創建時,就創建啓動好了線程,如果任務數量小於線程數的時候,會導致線程無意義的等待。我們不能很好的控制線程的數量
2,阻塞隊列滿了的時候,queue只能被阻塞,線程池應用速度變慢,沒有一個有效的機制來處理這種情況
三、JDK中的線程池和工作機制
1、線程池的創建及參數
線程池的創建主要依賴ThreadPoolExecutor類,它是jdk中所有線程池的父類。它的構造參數如下:
- int corePoolSize :線程池中核心線程數,< corePoolSize ,就會創建新線程,= corePoolSize ,這個任務就會保存到BlockingQueue,如果調用prestartAllCoreThreads()方法就會一次性的啓動corePoolSize 個數的線程。
- int maximumPoolSize, 允許的最大線程數,BlockingQueue也滿了,< maximumPoolSize時候就會再次創建新的線程
- long keepAliveTime, 線程空閒下來後,存活的時間,這個參數只在> corePoolSize纔有用
- TimeUnit unit, 存活時間的單位值
- BlockingQueue workQueue, 保存任務的阻塞隊列
- ThreadFactory threadFactory, 創建線程的工廠,給新建的線程賦予名字
- RejectedExecutionHandler handler :飽和策略,包括下面幾種
AbortPolicy :直接拋出異常,默認;
CallerRunsPolicy:用調用者所在的線程來執行任務
DiscardOldestPolicy:丟棄阻塞隊列裏最老的任務,隊列裏最靠前的任務
DiscardPolicy :當前任務直接丟棄
實現自己的飽和策略,實現RejectedExecutionHandler接口即可
2、提交任務的方法
execute(Runnable command) 不需要返回值
Future submit(Callable task) 需要返回值
3、關閉線程池的方法
shutdownNow():設置線程池的狀態,還會嘗試停止正在運行或者暫停任務的線程
shutdown()設置線程池的狀態,只會中斷所有沒有執行任務的線程
4、工作機制
來看一段重要的源碼:
如果小於核心線程數,就直接增加任務。否則就嘗試將任務放到等待隊列中,如果放失敗了,就在小於最大線程數的條件下進行新線程創建,如果仍然沒有成功,就拒絕任務。
四、合理配置線程池
線程池的使用也有它的合理性,並不是線程越多就越好,需要根據不同類型的任務來合理的配置線程池。
根據任務的性質來:
計算密集型(CPU),IO密集型,混合型
- 計算密集型:這類的主要任務有 加密,大數分解,正則……., 這類任務是計算量比較大,比較耗費cpu,應該儘量減少cpu在線程之間的輪轉,所以線程數適當小一點,最大推薦:機器的Cpu核心數+1,爲什麼+1,防止頁缺失,這是指當某個線程因爲調度原因不得不去等待下一個時間片時,+1的線程可以被執行 (機器的Cpu核心=Runtime.getRuntime().availableProcessors()😉
- IO密集型:讀取文件,數據庫連接,網絡通訊, 這類任務的特點是線程可能需要長時間等待。線程數適當大一點,可以充分的利用cpu,配置的線程數量可以參考機器的Cpu核心數*2
- 混合型:儘量拆分,拆分成一個IO密集型和一個計算密集型,但是當IO密集型需要的時間>>計算密集型需要的時間,拆分意義不大。當IO密集型所需時間和計算密集型差不多的時候,是非常有意義的。
隊列的選擇上,應該使用有界,無界隊列可能會導致內存溢出,OOM
五、預定義的線程池
jdk中給我們預定義了一些線程池,來方便特殊情況下的使用。
- FixedThreadPool
創建固定線程數量的,適用於負載較重的服務器,使用了無界隊列 - SingleThreadExecutor
創建單個線程,需要順序保證執行任務,不會有多個線程活動,使用了無界隊列 - CachedThreadPool
會根據需要來創建新線程的,執行很多短期異步任務的程序,使用了SynchronousQueue - WorkStealingPool(JDK7以後)
基於ForkJoinPool實現 - ScheduledThreadPoolExecutor
需要定期執行週期任務,Timer不建議使用了。
newSingleThreadScheduledExecutor:只包含一個線程,只需要單個線程執行週期任務,保證順序的執行各個任務
newScheduledThreadPool 可以包含多個線程的,線程執行週期任務,適度控制後臺線程數量的時候
方法說明:
schedule:只執行一次,任務還可以延時執行
scheduleAtFixedRate:提交固定時間間隔的任務
scheduleWithFixedDelay:提交固定延時間隔執行的任務
六、Executor框架
Executor的繼承關係大體如下:
Executor框架的基本使用流程:
下一篇:JAVA併發編程-9-併發安全