天天看點

ScheduledThreadPoolExecutor有坑嗷~

作者:Java解白
ScheduledThreadPoolExecutor有坑嗷~

概述

最近在做一些優化的時候用到了ScheduledThreadPoolExecutor。

雖然知道這個玩意,但是也很久沒用,本着再了解了解的心态,到網上搜尋了一下,結果就發現網上有些部落格在說ScheduledThreadPoolExecutor有巨坑!!!

ScheduledThreadPoolExecutor有坑嗷~

瞬間,我的興趣就被激起來了,馬上進去學習了一波~

不看不知道,看完後馬上把我的代碼坑給填上了~

ScheduledThreadPoolExecutor有坑嗷~

下面就當記錄一下吧,順便也帶大家了解了解,大家看完後也趕緊看看自己公司的項目代碼有沒有這種漏洞,有的話趕緊給填上,更新加薪指日可待!!!

ScheduledThreadPoolExecutor有坑嗷~

坑是啥?

先看下面案例代碼

public class ScheduledThreadPoolExecutorTest {

  public static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);
​
  public static AtomicInteger atomicInteger = new AtomicInteger(1);
​
  public static void main(String[] args) {
​
    scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
      // 模拟業務邏輯
      
      int num = atomicInteger.getAndIncrement();
      // 模拟出現異常
      if (num > 3) {
        throw new RuntimeException("定時任務執行異常");
      }
      
      System.out.println("别坑我!");
    }, 0, 1, TimeUnit.SECONDS);
​
    try {
      TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
​
    scheduledThreadPoolExecutor.shutdown();
​
  }
​
}
複制代碼           

案例代碼邏輯很簡單,主線程等待5秒後關閉線程池,定時任務執行三次後模拟抛出RuntimeException

但是我們看看執行結果,隻執行了三次!

因為某種情況下,定時任務在執行第四次時出現異常,進而導緻任務排程被取消,不會繼續執行

而且,異常資訊也沒有對外抛出!

ScheduledThreadPoolExecutor有坑嗷~
ScheduledThreadPoolExecutor有坑嗷~

那麼咋解決嘞?try-catch就行了呗~

ScheduledThreadPoolExecutor有坑嗷~

可以看到執行結果,雖然執行異常,但是任務卻還是一直在排程~

代碼裡使用工具類對Runnable任務包了一層,就是加了try-catch

public class RunnableDecoratorUtil {
​
   public static Runnable runnableDecorator(Runnable runnable) {
      return () -> {
         try {
            runnable.run();
         } catch (Exception e) {
            e.printStackTrace();
         }
      };
   }
​
}
複制代碼           

okok,總結一下,坑就是: 任務如果抛出異常就不會繼續排程執行了,趕緊去try-catch吧!!!

大家趕緊去看看自己代碼有沒有這個坑吧,本文到此結束!

ScheduledThreadPoolExecutor有坑嗷~

開個玩笑~ 光知道有坑哪能不知道為啥坑,接下來就帶大家了解一下坑到底是啥!

怎麼坑的?

直接進入scheduleAtFixedRate源碼檢視

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
  
    // 參數校驗
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
  
    // 将任務、執行時間、周期等封裝到ScheduledFutureTask内
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
  
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
  
    // 延時執行
    delayedExecute(t);
    return t;
}
複制代碼           

因為我們送出的任務被封裝在ScheduledFutureTask,是以我們直接來看ScheduledFutureTask的run方法

public void run() {
  // 校驗目前狀态是否還能執行任務,不能執行直接cancel取消
  if (!canRunInCurrentRunState(this))
    cancel(false);
  else if (!isPeriodic())
    // 如果不是周期性的,直接調用父類run方法執行一次即可
    super.run();
  else if (super.runAndReset()) { // 周期性任務,調用runAndReset運作并重置
    // 設定下一次的執行時間
    setNextRunTime();
    // 将任務重新加入隊列,進行排程
    reExecutePeriodic(outerTask);
  }
}
​
public boolean isPeriodic() {
  return period != 0;
}
複制代碼           

我們是周期性任務,是以直接看runAndReset源碼

protected boolean runAndReset() {
    // 檢查任務狀态,cas機制防止并發執行任務
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return false;
  
    // 預設不周期執行任務
    boolean ran = false;
    // state為NEW狀态
    int s = state;
    try {
        Callable<V> c = callable;
        if (c != null && s == NEW) {
            try {
                // 執行任務
                c.call();
                // 正常執行成功,設定為true代表周期執行
                ran = true;
            } catch (Throwable ex) {
                // 但是,如果執行異常!則不會将ran = true,是以最終傳回false
                setException(ex);
            }
        }
    } finally {
        runner = null;
        // 設定為NEW狀态
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
  
    // 正常執行完之後,結果為true,能夠周期執行
    // 但如果執行異常,ran為false,傳回結果為false
    return ran && s == NEW;
}
複制代碼           

通過上面源碼,我們可以很清楚的了解到,就是因為任務執行異常,且沒有被try-catch,是以導緻任務沒有被再次加入到隊列中進行排程。

并且通過文章開頭,我們還能看到任務執行異常,但是卻沒有抛出異常資訊

那是因為異常被封裝了,隻有調用get方法時,才會抛出異常

ScheduledThreadPoolExecutor有坑嗷~
​
/** The result to return or exception to throw from get() */
private Object outcome;
​
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
​
protected void setException(Throwable t) {
    if (STATE.compareAndSet(this, NEW, COMPLETING)) {
        // 将異常資訊指派給outcome
       // outcome既可以為任務執行結果也可以為異常資訊
        outcome = t;
        // 将state設定為異常狀态,state=3
        STATE.setRelease(this, EXCEPTIONAL); // final state
        finishCompletion();
    }
}
​
// 調用get方法阻塞擷取結果
public V get() throws InterruptedException, ExecutionException {
  int s = state;
  if (s <= COMPLETING)
    s = awaitDone(false, 0L);
  return report(s);
}
​
private V report(int s) throws ExecutionException {
  Object x = outcome;
  // 此時s = EXCEPTIONAL = 3
  if (s == NORMAL)
    return (V)x;
  if (s >= CANCELLED)
    throw new CancellationException();
  
  // 是以會走到這裡,對外抛出了任務執行的異常
  throw new ExecutionException((Throwable)x);
}
複制代碼           

總結

通過上面對源碼的了解,我們了解到,如果周期性任務執行出現異常,并且沒有被try-catch,會導緻該周期性任務不會再被放入到隊列中進行排程執行。

繼續閱讀