Java線程池的簡單瞭解
1 參考資料
- Java併發編程藝術
2 線程池執行原理
一圖勝千言,越來越覺得有道理,所有,我要上圖了~~
看圖之前要先知道Java線程池的組成(可以看圖二):
- 核心線程池-就是我們自己初始化線程池時設定的線程數目。(相當於正式員工)
- 阻塞隊列-核心線程池滿後就去排隊
- 最大線程池-隊列滿後纔去創建,可以創建的數目爲,最大線程池線程數-核心線程池線程數(相當於臨時工,任務太多了)
- 拒絕執行策略(任務更多了,又沒錢再招臨時工了,所以新的任務是放棄還是幹啥?)
這兩張圖都是對線程池的運行過程的描述,後一張是前面一張的更具體描述。
過程簡要描述如下:
提交一個需要被執行的任務時,
- 如果已經存在線程數目小於核心線程數,創建新線程執行任務,否則放入阻塞隊列
- 如果阻塞隊列沒滿,任務就在裏面等待
- 如果阻塞隊列滿了,就查看,當前線程數量是否小於最大線程數,如果小於,那就創建新線程
- 否則,那就交個飽和策略處理。
3 線程池初始化參數瞭解
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory and rejected execution handler.
* It may be more convenient to use one of the {@link Executors} factory
* methods instead of this general purpose constructor.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
看看JDK中的源碼,發現一句 It may be more convenient to use one of the {@link Executors} factory methods instead of this general purpose constructor.(使用 Executors 框架中封裝好的工廠方法會更方便 ,而不是使用這個通用的構造函數 )。所以,Executors 是更常用的線程池使用方法。
但是,阿里的Java開發手冊卻禁止使用Executors框架,推薦直接使用ThreadPoolExecutor
現在來看看參數(註釋已經寫得很清楚了,其實)
- corePoolSize
- maximumPoolSize
- keepAliveTime 超出核心線程數的那些線程當處於空閒時的保存時間(臨時工如果在規定時間內沒接到任務就辭退)
- unit ,keepAliveTime的時間單位
- workQueue 等待隊列
- ArrayBlockingQueue 基於數組結構的有界阻塞隊列
- LinkedBlockingQueue 基於鏈表結構的有界阻塞隊列
- SynchronousQueue 一個不存儲元素的阻塞隊列
- PriorityBlockingQueue 一個具有優先級的無界阻塞隊列
- Executors.defaultThreadFactory() 工廠方法,通過工廠方法給每個線程設置更有意義的名字
- defaultHandler 拒絕策略
- AbortionPolicy :直接拋出異常
- CallerRunsPolicy:只用調用者所在的線程運行任務
- DiscardOlderestPolicy:丟棄隊列裏最近的一個任務,然後執行當前任務
- DiscardPolicy:直接丟棄,不搭理
4 線程池的使用
4.1 創建
上面已經解釋了構造函數~
4.2 使用
主要有兩種方式:
- execute() 用於提交不需要返回值的任務
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
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);
}
- submit() 用於提交需要返回值的任務(submit方法是ExecuorService接口中的方法,通過Execotors類初始化的線程池會返回一個ExecuorService 對象)
4.3 關閉
原理:遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程。所以無法響應中斷的的任務可能永遠無法終止。
-
shutdown
shutdown只是將線程池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。但是,此時線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,纔會退出。
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); //將線程狀態設置爲SHUTDOWN interruptIdleWorkers(); // 中斷所有空間的線程 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); // 讓線程狀態轉變爲terminated }
-
shutdownNow
shutdownNow首先將線程池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的線程(通過Thead.interrupt() ),並返回等待執行任務的列表。
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); //將線程狀態設置爲stop interruptWorkers(); //中斷所有線程(不一定能中斷所有) tasks = drainQueue(); //獲取未執行的線程 } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown方法來關閉線程池,如果任務不一定要執行完,則可以調shutdownNow方法。
5 如何配置線程池
任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
性質不同的任務可以用不同規模的線程池分開處理。CPU密集型任務應配置儘可能小的線程,如配置N cpu +1個線程的線程池。由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如2*N cpu 。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。
建議使用有界隊列。有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點兒,比如幾千。
6 線程池監控
如果在系統中大量使用線程池,則有必要對線程池進行監控,方便在出現問題時,可以根據線程池的使用狀況快速定位問題。可以通過線程池提供的參數進行監控,在監控線程池的時候可以使用以下屬性。
- ·taskCount:線程池需要執行的任務數量。
- ·completedTaskCount:線程池在運行過程中已完成的任務數量,小於或等於taskCount。
- ·largestPoolSize:線程池裏曾經創建過的最大線程數量。通過這個數據可以知道線程池是否曾經滿過。如該數值等於線程池的最大大小,則表示線程池曾經滿過。
- ·getPoolSize:線程池的線程數量。如果線程池不銷燬的話,線程池裏的線程不會自動銷燬,所以這個大小隻增不減。
- ·getActiveCount:獲取活動的線程數。
通過擴展線程池進行監控。可以通過繼承線程池來自定義線程池,重寫線程池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。
7 一個小栗子
package threadpool;
import java.util.concurrent.*;
public class ExecutorsDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
Thread t1 = new Thread(new RunnableThread(),"runnable thread 1");
Thread t2 = new Thread(new RunnableThread(),"runnable thread 2");
Callable<String> t3 = new CallableThread("callable thread 1");
Callable<String> t4 = new CallableThread("callable thread 2");
executor.execute(t1);
executor.execute(t2);
Future f1 = executor.submit(t3);
Future f2 = executor.submit(t4);
String result1 = (String) f1.get();
String result2 = (String) f2.get();
System.out.println("result 1 = "+result1);
System.out.println("result 2 = "+result2);
executor.shutdown();
while (true){
if(executor.isTerminated() == true){
System.out.println("線程池關閉");
break;
}
}
}
}
class RunnableThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" is running and sleep 1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" is running and awake");
}
}
class CallableThread implements Callable{
private String name;
public CallableThread() {
}
public CallableThread(String name) {
this.name = name;
}
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+" (call) is running and sleep 1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" (call) is running and awake");
return this.name +" is over";
}
}
結果