java ThreadPoolExecutor線程池拒絕策略避坑
1.場景
線程池使用DiscardOldestPolicy拒絕策略,阻塞隊列使用ArrayBlockingQueue,發現在某些情形下對於得到的Future,調用get()方法當前線程會一直阻塞。
為瞭便於理解,將實際情景抽象為下面的代碼:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());//新建線程池時核心線程數及最大線程數都設置為1,阻塞隊列使用ArrayBlockingQueue,拒絕策略為DiscardOldestPolicy public void doBusiness(){ Task task1 = new Task(); Task task2 = new Task(); Task task3 = new Task(); Future<Boolean> future1 = threadPoolExecutor.submit(task1);//當前工作線程為0,會新建一個worker作為工作線程,並執行task1 Future<Boolean> future2 = threadPoolExecutor.submit(task2);//當前核心線程數已滿,會將任務放入阻塞隊列 Future<Boolean> future3 = threadPoolExecutor.submit(task3); /*當前核心線程已滿並且阻塞隊列已滿,execute()時會調用ThreadPoolExecutord的addWorker(command,false),由 於目前task1還沒執行完,則工作線程數量為1,已經達到瞭最大線程數,則addWorker(command,false)返回false, 觸發對應的拒絕策略,會從阻塞隊列中移除task2對應的任務(阻塞隊列中並不是直接放的task2,而是以task2為入 參構造的一個FutureTask,參見AbstarctExecutorService的submit(Callable<T> task)方法*/ try{ boolean result = future2.get(); System.out.println(result); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } @Test public void test_doBusiness(){ doBusiness();//入口 } private class Task implements Callable<Boolean>{ @Override public Boolean call() throws Exception { try { Thread.sleep(1000);//模擬業務執行 return true; }catch(Exception e){ e.printStackTrace(); } return true; } }
2. 原因分析
通過上面代碼我們明白瞭阻塞隊列會將task2對應的任務移除,那麼為何移除之後調用get()方法線程會一直阻塞呢?
其實Future future2= threadPoolExecutor.submit(task2)實際會調用AbstractExecutorService的submit(Callable task)方法,並且最終返回的future2實際是一個FutureTask類型。
public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); RunnableFuture<T> ftask = newTaskFor(task); execute(ftask); return ftask; }
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { return new FutureTask<T>(callable); }
因此,我們直接看FutureTask的get()方法
public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); }
由於future2已經從阻塞隊列中移除,並且從始至終都沒有工作線程執行它,即FutureTask的狀態一直都為NEW狀態,其會進入awaitDone(false,0L)中,接下列我們追蹤該方法。
private int awaitDone(boolean timed, long nanos) throws InterruptedException { final long deadline = timed ? System.nanoTime() + nanos : 0L; WaitNode q = null; boolean queued = false; for (;;) { if (Thread.interrupted()) { removeWaiter(q); throw new InterruptedException(); } int s = state; if (s > COMPLETING) { if (q != null) q.thread = null; return s; } else if (s == COMPLETING) // cannot time out yet Thread.yield(); else if (q == null)//第一次進for循環時q==null,進入到該分支 q = new WaitNode(); else if (!queued)//第二次進for循環時queue為false,則使用CAS將q置為waiters的頭結點 queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); else if (timed) { nanos = deadline - System.nanoTime(); if (nanos <= 0L) { removeWaiter(q); return state; } LockSupport.parkNanos(this, nanos); } else//將q置為頭結點後,最終會進入這裡調用park()方法,阻塞當前線程 LockSupport.park(this); }
從上面的代碼可以看出調用future2.get()後會一直阻塞在park()方法處,這便是本次問題出現的原因,
3.總結
本次問題出現主要是同時滿足瞭以下幾點:
1)使用瞭有界的阻塞隊列ArrayBlockingQueue
2)工作線程達到瞭線程池配置的最大線程數
3)拒絕策略使用瞭DiscardOldestPolicy(使用DiscardPolicy也會出現這個問題)
4.思考
我們日常使用線程池提交任務後,如果在任務執行完成之前調用future的get()方法,當前線程會進入阻塞狀態,當任務執行完成後,才會將當前線程喚醒,如何從代碼上分析該流程?
首先當任務提交到線程池,如果任務當前在阻塞隊列中,則FutureTask的狀態依然像上面的情況一樣,是處於New狀態,調用get()方法依然會到達LockSupport.park(this)處,將當前線程阻塞。什麼時候才會將當前線程喚醒瞭?
那就是當存在工作線程Worker目前分配的任務執行完成後,其會去調用Worker類的getTask()方法從阻塞隊列中拿到該任務,並執行該任務的run()方法,下面是FutureTask的run()方法
public void run() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) set(result);//如果任務執行成功,則調用set(V result)方法 } } finally { // runner must be non-null until state is settled to // prevent concurrent calls to run() runner = null; // state must be re-read after nulling runner to prevent // leaked interrupts int s = state; if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); } }
其會在執行成功後,調用set(V result)方法
protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = v; UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state finishCompletion();// } }
然後將FutureTask狀態置為NORMAL(FutureTask的狀態要和ThreadPoolExecutor的狀態區分開),接著調用finishCompletion()方法
private void finishCompletion() { // assert state > COMPLETING; for (WaitNode q; (q = waiters) != null;) { if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { for (;;) { Thread t = q.thread;//q在await()方法中設置的,其值為調用get()方法的線程 if (t != null) { q.thread = null; LockSupport.unpark(t);//喚醒該線程 } WaitNode next = q.next; if (next == null) break; q.next = null; // unlink to help gc q = next; } break; } } done();//熟悉的鉤子方法 callable = null; // to reduce footprint }
在finishCompletion中喚起因get()而阻塞的線程。
以上就是java ThreadPoolExecutor線程池拒絕策略避坑的詳細內容,更多關於java ThreadPoolExecutor的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 解析Java異步之call future
- Java多線程 CompletionService
- Java線程的異常處理機制詳情
- Future cancel迷惑性boolean入參解析
- Java使用Runnable和Callable實現多線程的區別詳解