Java高併發系列5-線程池
接上一篇Java併發系列4-併發容器我們繼續
在編程中經常會使用線程來異步處理任務,但是每個線程的創建和銷燬都需要一定的開銷。如果每次執行一個任務都需要開個新線程去執行,則這些線程的創建和銷燬將消耗大量的資源;並且很難對其單個線程進行控制,更何況有一堆的線程在執行。這時就需要線程池來對線程進行管理。
在線程池的管理下,線程分爲啓動,執行,空閒狀態, 如果新來任務則將任務交給空閒線程執行即可。 先看一條程序來了解一下線程池
mport java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(5); //execute submit
for (int i = 0; i < 6; i++) {
service.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
System.out.println(service);
service.shutdown(); // 關閉線程, 但是不會立即終止線程池中的線程。 而是先標記Stop狀態,停止接受線程,直到所有線程均執行完畢。
/// service.shutdownNow(); /// 立刻關閉線程池, 嘗試打斷所有在執行的線程。 並將狀態標記爲Shutdown 。
System.out.println(service.isTerminated()); /// CPU是否空閒, 只要線程池中有任務在執行 就是false
System.out.println(service.isShutdown()); /// 是否是隻要調用上邊任意一個shutdown 方法 就返回true
System.out.println(service);
TimeUnit.SECONDS.sleep(5);
System.out.println(service.isTerminated());
System.out.println(service.isShutdown());
System.out.println(service);
}
}
Executor 和 ExecutorService 和 Executors
看一下 Executor
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);
}
這個類是Executor ,實現的方法 也只有 execute 方法 。接受一個Runnable ,實現可以參考該類的註釋。
*
直接執行
* <pre> {@code
* class DirectExecutor implements Executor {
* public void execute(Runnable r) {
* r.run();
* }
* }}</pre>
*
* More typically, tasks are executed in some thread other than the
* caller's thread. The executor below spawns a new thread for each
* task.
* //// 創建一個新的線程來執行runable的r
* <pre> {@code
* class ThreadPerTaskExecutor implements Executor {
* public void execute(Runnable r) {
* new Thread(r).start();
* }
* }}</pre>
*
繼續看, 精簡一下 ,列出了主要的方法
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
...
}
我們大概知道了 該方法就是線程池的大概的框架接口, 提供的 提交任務,結束任務,池狀態類。 仔細看就會發現 submit 不僅可以接受Runable ,也可以接受Callable 爲參數 。
現在大概說一下 Runnable 與Callable 與 FutureTask 的區別
主要區別
Callable提供了泛型T作爲任務執行的結果 。
而Runnable沒有任務返回值。
FutureTask 看類結構 代碼這裏就不再粘了, 不作爲本文的重點展開描述。
FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口繼承自 Runnable , Future . 用大腿想一下 大概也就知道了, 這個類就擁有了 Runnable 和Future 共同的特性,並且可以接收Callable爲參數 構建FutureTask 來執行。
再說一下 Future , Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。
-
cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning爲true還是false,肯定返回true。
-
isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
-
get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;
-
get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。
舉個生活場景來描述這一部分應用。
早晨到蛋糕店買蛋糕(有目的性的任務),告訴店員蛋糕需求(長寬,需求,味道等),店員告訴你要下午才能做好,並給你了個取蛋糕收條(Future<蛋糕>) , 結果下午你提前到了,拿着蛋糕收條取蛋糕 調用 get() ,由於提前到了,所以人還沒做好,只能扣手機了。 這時你是阻塞在這裏,等待蛋糕做好取走。 當然這時可以設置等待時間,比如我只等30分鐘,超時沒做好 ,太慢了沒做好 大爺的不要了(土豪)。 這就是提交submit ,執行任務, 等待獲取get到蛋糕。
再來看一下Executors ,看一下Executors 的使用方式, newFixedThreadPool, newWorkStealingPool,newFixedThreadPool,newSingleThreadExecutor ,newCachedThreadPool 等創建線程池的方式。 大概就明白了這玩意兒和Arrays差不多, Arrays 對數組的各種操作, 集合。
常見的線程池使用大概有四種 看了實現調用方法, 都是從ThreadPoolExecutor 的構造函數中配置不同的參數而構造的線程池。
看一下 ThreadPoolExecutor 共有四個構造函數, 我們看一下最全的參數最多的構造函數 , 瞜一眼代碼
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
///////核心線程數。默認情況下線程池是空的,只有任務提交時纔會創建線程。如果當前運
行的線程數少於corePoolSize,則創建新線程來處理任務;如果等於或者多於corePoolSize,則不再創建。如
果調用線程池的 prestartAllcoreThread方法,線程池會提前創建並啓動所有的核心線程來等待任務。
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
////線程池允許創建的最大線程數。如果任務隊列滿了並且線程數小於
maximumPoolSize時,則線程池仍舊會創建新的線程來處理任務
* @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.
如果當前線程數大於corePoolSize,則將任務添加到此任務隊列中。該任務
隊列是BlockingQueue類型的,也就是阻塞隊列。
* @param threadFactory the factory to use when the executor
* creates a new thread , 線程工廠,
可以用線程工廠給每個創建出來的線程設置名字。一般情況下無須設置
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
線程飽和策略, 這是當任務隊列和線程池都滿了時所採取的應對策略,默認
是AbordPolicy,表示無法處理新任務,並拋出RejectedExecutionException異常。
* @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}
* or {@code threadFactory} or {@code handler} is null
*/
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;
}
ThreadPoolExecutor 構造函數的意義都已明白,再看一下線程池的工作流程。
- 提交任務到線程池, 提交任務後,線程池先判斷線程數是否達到了核心線程數(corePoolSize)。如果沒有達到核心線程數,則創建核心線程來執行該任務。 如果達到了核心線程數,轉2 。
- 則判斷任務隊列是否已滿,沒有滿則將任務添加至任務列表等待。 如果滿了轉3.
- 如果滿了則判斷是否達到了最大線程數。 如果沒有達到最大線程數則創建非核心線程來執行此任務。 如果達到了最大線程數則執行handler ,即飽和策略。
還有一個類
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService{
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
}
這個類 雖然起了個新名字,但結構也簡單 ,實現繼承自ThreadPoolExecutor , 構造函數使用的也是父類的構造函數, 值得注意的一點是,使用的任務隊列是DelayQueue ,簡單說一下 DelayQueue。
DelayQueue 是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現
Delayed 接口。創建元素時,可以指定元素到期的時間,只有在元素到期時才能從隊列中取走。
使用這樣的隊列就給ScheduledThreadPoolExecutor 的Scheduled 起到了關鍵性作用。
接下來大概看一下這幾種常見的線程池。
- FixedThreadPool
FixedThreadPool 是可重用固定線程數的線程池。我們看到核心線程數和最大線程數是一樣的,意味着該方式創建的線程池只有核心線程。沒有非核心線程。keepAliveTime設置爲0L意味着多餘的線程會被立即終止。因爲不會產生多餘的線程,所以keepAliveTime是無效的參數。另外,任務隊列採用了無界的阻塞隊列LinkedBlockingQueue。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
執行任務流程大概是
如果當前運行的線程未達到corePoolSize(核心線程數)時就創建核心線程來處理任務,如果達到了核心線程數則將任務添加到LinkedBlockingQueue中。FixedThreadPool就是一個有固定數量核心線程的線程池,並且這些核心線程不會被回收。當線程數超過corePoolSize 時,就將任務存儲在任務隊列中;當線程池有空閒線程時,則從任務隊列中去取任務執行。同時無界的LinkedBlockingQueue保證新任務都能夠放入隊列,不會被拒絕。添加到不能添加爲止。
- SingleThreadExecutor
SingleThreadExecutor是使用單個工作線程的線程池,構造函數調用方法如下代碼。
corePoolSize和maximumPoolSize都爲1,意味着SingleThreadExecutor只有一個核心線程,其他的參數都
和FixedThreadPool一樣,這裏就不贅述了。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
值得一提的是因爲這種創建方式是,核心線程和總線程數量都爲1,如果當前運行的線程數未達到核心線程數,也就是當前沒有運行的線程,則創建一個新線程來處理任務。如果當前有運行的線程,則將任務添加到阻塞隊列LinkedBlockingQueue中。因此,SingleThreadExecutor能確保所有的任務在一個線程, 所以所有提交的線程只能按照隊列順序依次執行。
SingleThreadExecutor 適用於在邏輯上需要單線程處理任務的場景,同時無界的LinkedBlockingQueue保證新任務都能夠放入隊列,不會被拒絕;缺點和FixedThreadPool相同,當處理任務無限等待的時候會造成內存問題。
- CachedThreadPool
CachedThreadPool是一個根據需要創建線程的線程池,構建線程池的構造函數如下。corePoolSize爲0,maximumPoolSize設置爲Integer.MAX_VALUE,這意味着CachedThreadPool沒有核心線程,非核心線程是無界的。keepAliveTime設置爲60L,則空閒線程等待新任務的最長時間爲 60s。同時使用的任務隊列是SynchronousQueue同步隊列,這就意味着線程池的數量無限大,新任務會直接分配或者創建一個線程進行執行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
首先會執行SynchronousQueue的offer方法來提交任務,並且查詢線程池中是否有空閒的線程執行SynchronousQueue的poll方法來移除任務。如果有則配對成功,將任務交給這個空閒的線程處理;如果沒有則配對失敗,創建新的線程去處理任務。當線程池中的線程空閒時,它會執行SynchronousQueue的poll方法,等待SynchronousQueue中新提交的任務。如果超過 60s 沒有新任務提交到
SynchronousQueue,則這個空閒線程將終止。因爲maximumPoolSize 是無界的,所以如果提交的任務大於線
程池中線程處理任務的速度就會不斷地創建新線程。另外,每次提交任務都會立即有線程去處理。
所以 CachedThreadPool 比較適於大量的需要立即處理並且耗時較少的任務。 例如搞個棋牌室遊戲,後臺需要對每個人打的牌進行立刻轉發。 使用CachedThreadPool比較高效。
- ScheduledThreadPool
ScheduledThreadPool是一個能實現定時和週期性任務的線程池,構造函數參數傳遞,實際就是調用了ThreadPoolExecutor的構造函數。
corePoolSize是傳進來的固定數值,maximumPoolSize的值是Integer.MAX_VALUE。因爲採用的DelayedWorkQueue是無界的,所以maximumPoolSize這個參數是無效的。
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
當執行 ScheduledThreadPoolExecutor 的 scheduleAtFixedRate 或者 scheduleWithFixedDelay方法時,會向DelayedWorkQueue 添加一個 實現 RunnableScheduledFuture 接口的ScheduledFutureTask(任務的包裝類),並會檢查運行的線程是否達到 corePoolSize。如果沒有則新建線程並啓動它,但並不是立即去執行任務,而是去 DelayedWorkQueue 中取ScheduledFutureTask,然後去執行任務。如果運行的線程達到了corePoolSize時,則將任務添加到DelayedWorkQueue中。DelayedWorkQueue會將任務進行排序,先要執行的任務放在隊列的前面。
其跟此前介紹的線程池不同的是,當執行完任務後,會將ScheduledFutureTask中的time變量改爲下次要執行的時間並放回到DelayedWorkQueue中。
/**
* Returns the nanoTime-based trigger time of a delayed action.
*/
long triggerTime(long delay) {
return System.nanoTime() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
////////// 將設置好delay時間的task ,添加至任務列表。
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
最後再看一下飽和策略 RejectedExecutionHandler 。這是當任務隊列和線程池都滿了時所採取的應對策略,默認
是AbordPolicy,表示無法處理新任務,並拋出RejectedExecutionException異常。此外還有3種策略,它們分
別如下。
(1)CallerRunsPolicy:用調用者所在的線程來處理任務。此策略提供簡單的反饋控制機制,能夠減緩
新任務的提交速度。
(2)DiscardPolicy:不能執行的任務,並將該任務刪除。
(3)DiscardOldestPolicy:丟棄隊列最近的任務,並執行當前的任務
當然我們也可以自定義飽和策略。
我們也可以通過修改 ThreadPoolExecutor的構造函數來自定義任務處理策略。例如面對的業務是將數據異步寫入HBase,當HBase嚴重超時的時候允許寫入失敗並記錄日誌以便事後補寫。對於這種應用場景,如果使用FixedThreadPool,在HBase服務嚴重超時的時候會導致隊列無限增長,引發內存問題;如果使用CachedThreadPool,會導致線程數量無限增長。對於這種場景,我們可以設置ExecutorService使用帶有長度限制的隊列以及限定最大線程個數的線程池,同時通過設置RejectedExecutionHandler處理任務被拒絕的情況。
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 處理任務被拒絕的情況,例如記錄日誌等
}
}
再來幾種不太常用的
- ForkJoinPool是一種支持任務分解的線程池,當提交給他的任務“過大”,他就會按照預先定義的規則將大任務分解成小任務,多線程併發執行。一般要配合可分解任務接口ForkJoinTask來使用,ForkJoinTask有兩個實現它的抽象類:RecursiveAction和RecursiveTask,其區別是前者沒有返回值,後者有返回值。
來段demo 看下
public class ForkJoinPool {
static int[] nums = new int[1000000];
static final int MAX_NUM = 50000;
static Random r = new Random();
static {
for(int i=0; i<nums.length; i++) {
nums[i] = r.nextInt(100);
}
System.out.println(Arrays.stream(nums).sum()); //stream api
}
/*
static class AddTask extends RecursiveAction {
int start, end;
AddTask(int s, int e) {
start = s;
end = e;
}
@Override
protected void compute() {
if(end-start <= MAX_NUM) {
long sum = 0L;
for(int i=start; i<end; i++) sum += nums[i];
System.out.println("from:" + start + " to:" + end + " = " + sum);
} else {
int middle = start + (end-start)/2;
AddTask subTask1 = new AddTask(start, middle);
AddTask subTask2 = new AddTask(middle, end);
subTask1.fork();
subTask2.fork();
}
}
}
*/
static class AddTask extends RecursiveTask<Long> {
int start, end;
AddTask(int s, int e) {
start = s;
end = e;
}
@Override
protected Long compute() {
/// 當分配的數值區間,小於分配最大閾值 則進行任務fork ,
if(end-start <= MAX_NUM) {
long sum = 0L;
for(int i=start; i<end; i++) sum += nums[i];
return sum;
}
int middle = start + (end-start)/2;
AddTask subTask1 = new AddTask(start, middle);
AddTask subTask2 = new AddTask(middle, end);
subTask1.fork();
subTask2.fork();
/// 將執行結果join在一起
return subTask1.join() + subTask2.join();
}
}
public static void main(String[] args) throws IOException {
ForkJoinPool fjp = new ForkJoinPool();
AddTask task = new AddTask(0, nums.length);
fjp.execute(task);
fjp.awaitTermination(20, TimeUnit.SECONDS);//等待20s,觀察結果
long result = task.join();
System.out.println(result);
fjp.shutdown();
//System.in.read();
}
}
代碼勝千言,簡單明瞭。不再贅述。
- WorkStealingPool
(1)Steal,翻譯爲偷竊,竊取。這裏的意思是,如果當前工作線程處理完自己本地任務隊列中的任務時,就會去全局隊列或者其他工程線程的隊列裏面查找工作任務,幫助它們完成。
(2)利用Work Staling,可以更好實現負載均衡。因爲每個工作線程的任務都是不一樣的,完成的時間也不一樣。
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
來一段demo
public class WorkStealingPool {
public static void main(String[] args) throws IOException {
ExecutorService service = Executors.newWorkStealingPool();
System.out.println(Runtime.getRuntime().availableProcessors());
service.execute(new R(1000));
service.execute(new R(2000));
service.execute(new R(2000));
service.execute(new R(2000)); //daemon
service.execute(new R(2000));
//由於產生的是精靈線程(守護線程、後臺線程),主線程不阻塞的話,看不到輸出
System.in.read();
}
static class R implements Runnable {
int time;
R(int t) {
this.time = t;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(time + " " + Thread.currentThread().getName());
}
}
}
-
這個線程池的優點:
(1)當添加任務時,喚醒一個空閒的工作線程,而不是一羣線程,所以不會產生驚羣現象。
(2)Work stealing pool,每個工作線程有自己的任務隊列,當前完成自己本地的隊列的任務時,會自動去全局隊列裏面獲取任務來工作,或者去”偷“其他線程的隊列裏面的任務。
(3)當添加任務時,沒有直接就把任務集中放在全局隊列裏面,避免工作線程集中去全局隊列裏面獲取任務而造成頻繁的鎖開銷。 -
這個線程的缺點:
這個線程池有一個很明顯的缺陷,就是,如果線程池裏只有一個線程時,所添加的工作任務不支持任務遞歸,什麼意思呢?就是說,在線程所要執行的工作任務,不能再添加新的工作任務到線程池中,否則,會造成死鎖。
爲什麼會有這個問題呢?
其實,跟這個線程池的實現有很大關係(這不是廢話嘛),線程在執行任務時,用了加鎖操作,而且只有在當前任務執行完成後才通過信號量的方式通知主線程(等待結果的線程)計算結果已經完成了,所以,如果在任務中遞歸執行添加新的任務在線程池中,就會造成死鎖,因爲第一個在執行第一個任務之前就鎖住了線程。
一些可能的解決辦法:
要怎麼解決這個問題呢?一個可能性的解決方法是,對應這種內部的任務,另外開一個線程去執行。不過,因爲時間的關係,我還沒有試過。
總結:
- 對於希望提交的任務儘快分配線程執行,使用CachedThreadPool
- 對於需要保證所有提交的任務都要被執行的情況,使用FixedThreadPool
- 如果限定只能使用一個線程進行任務處理,使用SingleThreadExecutor
如果業務上允許任務執行失敗,或者任務執行過程可能出現執行時間過長進而影響其他業務的應用場景,可以通過使用限定線程數量的線程池以及限定長度的隊列進行容錯處理。
參考資料:
https://blog.csdn.net/xhjcehust/article/details/45844901
https://www.cnblogs.com/ok-wolf/p/7761755.html