前言
最近在看并發編程藝術這本書,對看書的一些筆記及個人工作中的總結。
線程池的優勢
Java中的線程池是運用場景最多的并發框架,幾乎所有需要異步或并發執行任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來3個好處。
第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用線程池,必須對其實現原理了如指掌。
線程池的實現原理
提交一個新的任務到線程池中,線程池的處理流程:
1)線程池判斷核心線程池里的線程是否都在執行任務。如果不是(也就是當前執行任務的線程數小于核心線程數corePoolSize),則創建一個新的線程執行任務。如果線程池中的線程都在執行任務,也就是說當前的工作線程數大于等于corePoolSize),則進入下個流程
2)線程池判斷工作隊列(一般是阻塞隊列BlockingQueue)是否已經滿了。如果工作隊列沒有滿,則將任務增加到工作隊列中。如果工作隊列滿了,則進入下個流程,
3)線程池判斷線程池的線程是否都處于工作狀態。如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略(或者叫做拒絕策略)來處理這個任務。
ThreadPoolExecutor執行execute方法分下面4種情況。
1)如果當前運行的線程少于corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
2)如果運行的線程等于或多于corePoolSize,則將任務加入BlockingQueue。
3)如果無法將任務加入BlockingQueue(隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。
4)如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,并調用RejectedExecutionHandler.rejectedExecution()方法。
下面是ThreadPoolExecutor的一個構造方法:
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;
}
稍微分析一下這幾個參數,官方api都有講:
corePoolSize:當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大于線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建并啟動所有基本線程。
maximumPoolSize:線程池中允許的最大線程數。如果隊列滿了,并且已創建的線程數小于最大線程數,則線程池會再創建新的線程執行任務。如果使用的是無界的隊列,那么這個參數沒有意義了。
keepAliveTime:線程池工作線程空閑后,保持的存活時間。
unit :上面時間的單位
workQueue:執行任務在執行之前加入的queue。該隊列僅保存由execute方法提交的Runnable任務。
threadFactory:創建新線程的工廠方法。可以通過線程工廠給每個創建的線程設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給線程池里的線程設置有意義的名字
handler:當隊列滿時,線程處于飽和策略。拒絕策略handler
jdk1.5提供了下面幾種策略:
AbortPolicy:直接拋異常。
DiscardPolicy:顧名思義,直接丟棄多余的任務。
DiscardOldestPolicy:丟棄隊列中最老的任務,并且重試exexute方法,并發書中說丟棄最新的一個任務,這邊書中應該描述錯了。
CallerRunsPolicy:在調用線程中執行丟棄的任務。
也可以自定義拒絕策略,比如記錄日志或者持久化處理。
執行源碼:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
執行線程次提交的任務
通過execute方法和submit方法(submit提供了重載方法)。execute不返回值,而submit返回值,并且可以通過返回的Future對象的get方法查看返回值。
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
定義在ExecutorService類中:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
//BlockingQueue使用有界阻塞隊列
public class ThreadPoolExecutorTest1 {
public static void main(String[] args) {
/**
* 在使用有界隊列時,若有新的任務需要執行,如果線程池實際線程數小于corePoolSize,則優先創建線程,
* 若大于corePoolSize,則會將任務加入隊列,
* 若隊列已滿,則在總線程數不大于maximumPoolSize的前提下,創建新的線程,
* 若線程數大于maximumPoolSize,則執行拒絕策略。或其他自定義方式。
*
* 這個構造方法使用默認的拒絕策略,AbortPolicy(即拋出異常)
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, //coreSize
2, //MaxSize
60, //60
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3) //指定一種隊列 (有界隊列)
);
MyTask mt1 = new MyTask(1, "任務1");
MyTask mt2 = new MyTask(2, "任務2");
MyTask mt3 = new MyTask(3, "任務3");
MyTask mt4 = new MyTask(4, "任務4");
MyTask mt5 = new MyTask(5, "任務5");
//MyTask mt6 = new MyTask(6, "任務6");
pool.execute(mt1);
pool.execute(mt2);
pool.execute(mt3);
pool.execute(mt4);
pool.execute(mt5);
//pool.execute(mt6);
pool.shutdown();
}
}
public class MyTask implements Runnable {
private int taskId;
private String taskName;
public MyTask(int taskId, String taskName){
this.taskId = taskId;
this.taskName = taskName;
}
@Override
public void run() {
try {
System.out.println("run taskId =" + this.taskId);
Thread.sleep(5*1000);
//System.out.println("end taskId =" + this.taskId);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String toString(){
return Integer.toString(this.taskId);
}
}
使用無界BlockingQueue:
與有界隊列相比,除非系統資源耗盡,否則無界的隊列不存在任務入隊失敗的情況。當有新任務到來,系統的線程小于corePoolSize時,則新建線程執行任務。當達到corePoolSize后,就不會繼續增加。若后續仍有新的隊列加入,而沒有空閑的線程資源,則隊列直接進入隊列等待。若任務創建和處理的速度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。
所以一般不建議使用無界隊列
//使用無界隊列的時候,最大線程數,拒絕策略等都失去了意義
public class ThreadPoolExecutorTest2 implements Runnable{
private static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
try {
int temp = count.incrementAndGet();
System.out.println("任務" + temp);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
BlockingQueue<Runnable> queue =
new LinkedBlockingQueue<>();
ExecutorService executor = new ThreadPoolExecutor(5, 10, 120L, TimeUnit.SECONDS, queue);
for(int i = 0 ; i < 100; i++){
executor.execute(new ThreadPoolExecutorTest2());
}
Thread.sleep(1000);
System.out.println("queue size:" + queue.size());
Thread.sleep(2000);
}
}
自定義拒絕策略:
//和第一個demo相比,因為默認使用的策略是AbortPolicy(拋出一個拒絕異常),而我們這個使用了自定義的拒絕策略
public class ThreadPoolExecutorTest3 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, //coreSize
2, //MaxSize
60, //60
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3) //指定一種隊列 (有界隊列)
, new MyRejected()
// , new DiscardOldestPolicy()
);
MyTask mt1 = new MyTask(1, "任務1");
MyTask mt2 = new MyTask(2, "任務2");
MyTask mt3 = new MyTask(3, "任務3");
MyTask mt4 = new MyTask(4, "任務4");
MyTask mt5 = new MyTask(5, "任務5");
MyTask mt6 = new MyTask(6, "任務6");
pool.execute(mt1);
pool.execute(mt2);
pool.execute(mt3);
pool.execute(mt4);
pool.execute(mt5);
pool.execute(mt6);
pool.shutdown();
}
}
public class MyRejected implements RejectedExecutionHandler {
public MyRejected(){
}
//拒絕策略實際調用的方法處理丟棄的任務,這邊可以持久化,可以進行日志處理
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定義處理..");
System.out.println("當前被拒絕任務為:" + r.toString());
}
}
下一篇博客將會分析一下jdk提供的Executor框架創建線程池的幾種底層實現原理。