Future.get卡死画饥,線程池的一個坑點

如果線程池的拒絕策略設(shè)置成DiscardPolicy或者DiscardOldestPolicy衔瓮,通過Future獲取執(zhí)行結(jié)果,可能導(dǎo)致線程會一直阻塞荒澡。

問題復(fù)現(xiàn)

  // 創(chuàng)建一個單線程报辱,拒絕策略時 DiscardPolicy
  private final static ThreadPoolExecutor executorService = new
      ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES,
      new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.DiscardPolicy());

  public static void main(String[] args) throws Exception {
    //提交任務(wù),阻塞 5 秒
    Future taskOne = executorService.submit(() -> {
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    });
    //此時单山,隊列和線程已經(jīng)都被占用碍现,當(dāng)前提交的任務(wù)會執(zhí)行拒絕策略
    Future taskTwo = null;
    try {
      taskTwo = executorService.submit(() -> System.out.println("start runable three"));
    } catch (Exception e) {
      System.out.println(e.getLocalizedMessage());
    }
    System.out.println("獲取結(jié)果:");
    System.out.println("task one " + taskOne.get()); //(5)等待任務(wù)one執(zhí)行完畢
    System.out
        .println("task two " + (taskTwo == null ? null : taskTwo.get())); // (7)等待任務(wù)three執(zhí)行完畢
    executorService.shutdown(); //關(guān)閉線程池,阻塞直到所有任務(wù)執(zhí)行完畢
  }

執(zhí)行結(jié)果如下米奸,第一個task正持缃樱可以獲取結(jié)果,但是第二個task一直獲取不到結(jié)果悴晰,程序一直卡在這里慢睡,不會繼續(xù)執(zhí)行。

獲取結(jié)果:
task one null

問題分析

提交任務(wù)到線程池時铡溪,會包裝成 FutureTask 漂辐,初始狀態(tài)是 NEW。執(zhí)行的任務(wù)是包裝后的FutureTask對象棕硫。

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    // 包裝成 FutureTask
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

提交執(zhí)行任務(wù)方法邏輯如下髓涯。

public void execute(Runnable command) {
  ...
  //如果線程個數(shù)小于核心線程數(shù)則新增處理線程
  int c = ctl.get();
  if (workerCountOf(c) < corePoolSize) {
      if (addWorker(command, true))
          return;
      c = ctl.get();
  }
  // 如果當(dāng)前線程個數(shù)已經(jīng)達(dá)到核心線程數(shù)則把任務(wù)放入隊列
  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); //新增失敗則調(diào)用拒絕策略
}

示例代碼中第二個任務(wù)會執(zhí)行到reject邏輯。DiscardPolicy的方法是空實現(xiàn)哈扮,所以新創(chuàng)建的FutureTask還是NEW狀態(tài)纬纪,這個狀態(tài)和get方法阻塞有密切的關(guān)系蚓再。

DiscardPolicy 和 DiscardOldestPolicy 代碼如下。他們有一個共同點就是沒有處理task的狀態(tài)包各。

public static class DiscardPolicy implements RejectedExecutionHandler {
    /**
     * 空方法摘仅,task會保留在NEW狀態(tài)
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    /**
     * poll 出一個任務(wù),但是沒有任務(wù)處理问畅,所以poll出來的任務(wù)是NEW狀態(tài)
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}    

先看下 FutureTask 的狀態(tài)娃属。前面我們看到了初始化狀態(tài)是NEW,其他狀態(tài)說明如下按声。

private static final int NEW          = 0; 新的任務(wù)膳犹,初始狀態(tài)
private static final int COMPLETING   = 1; 當(dāng)任務(wù)被設(shè)置結(jié)果時,處于COMPLETING狀態(tài)签则,這是一個中間狀態(tài)须床。
private static final int NORMAL       = 2; 表示任務(wù)正常結(jié)束。
private static final int EXCEPTIONAL  = 3; 表示任務(wù)因異常而結(jié)束
private static final int CANCELLED    = 4; 任務(wù)還未執(zhí)行之前就調(diào)用了cancel(true)方法渐裂,任務(wù)處于CANCELLED
private static final int INTERRUPTING = 5; 當(dāng)任務(wù)調(diào)用cancel(true)中斷程序時豺旬,任務(wù)處于INTERRUPTING狀態(tài),這是一個中間狀態(tài)柒凉。
private static final int INTERRUPTED  = 6; 任務(wù)調(diào)用cancel(true)中斷程序時會調(diào)用interrupt()方法中斷線程運行族阅,任務(wù)狀態(tài)由INTERRUPTING轉(zhuǎn)變?yōu)镮NTERRUPTED

繼續(xù)看下 FutureTask 的get方法。

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    //當(dāng)狀態(tài)值<=COMPLETING時需要等待膝捞,否則調(diào)用report返回
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
private V report(int s) throws ExecutionException {
    Object x = outcome;
    // 正常結(jié)束坦刀,返回結(jié)果
    if (s == NORMAL)
        return (V)x;
    // 如果是 >= CANCELLED 拋出取消異常,包括:CANCELLED蔬咬,INTERRUPTING鲤遥,INTERRUPTED狀態(tài)
    if (s >= CANCELLED)
        throw new CancellationException();
    // 剩下的條件就是 EXCEPTIONAL 了,執(zhí)行的任務(wù)拋出異常
    throw new ExecutionException((Throwable)x)
}

到這里已經(jīng)很清楚了林艘。FutureTask狀態(tài)>COMPLETING 才會返回盖奈。因為拒絕策略沒有修改FutureTask的狀態(tài),F(xiàn)utureTask的狀態(tài)一直是NEW狐援,所以不會返回钢坦。

其他 RejectedExecutionHandler 為什么不會導(dǎo)致阻塞

我看看下默認(rèn)的 AbortPolicy 的實現(xiàn):

public static class AbortPolicy implements RejectedExecutionHandler {
    // 回憶一下submit方法,最后會執(zhí)行reject策略啥酱。
    // AbortPolicy 直接拋出異常爹凹,調(diào)用方馬上可以獲取結(jié)果
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

CallerRunsPolicy 策略則是讓調(diào)用線程執(zhí)行提交的任務(wù),執(zhí)行任務(wù)時會更新狀態(tài)镶殷,自然也不會阻塞禾酱。

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

解決方案

  1. 使用帶超時時間的get方法,這樣使用DiscardPolicy拒絕策略不會一直阻塞。
  2. 如果一定要使用Discardpolicy 拒絕策略宇植,需要自定義拒絕策略。
public void rejectedExecution(Runnable runable, ThreadPoolExecutor e) {
    if (! e.isShutdown()) {
        if(null ! = runable && runable instanceof FutureTask){
            ((FutureTask) runable).cancel(true);
          }
      }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末埋心,一起剝皮案震驚了整個濱河市指郁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拷呆,老刑警劉巖闲坎,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茬斧,居然都是意外死亡腰懂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門项秉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绣溜,“玉大人,你說我怎么就攤上這事娄蔼〔烙鳎” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵岁诉,是天一觀的道長锚沸。 經(jīng)常有香客問我,道長涕癣,這世上最難降的妖魔是什么哗蜈? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮坠韩,結(jié)果婚禮上距潘,老公的妹妹穿的比我還像新娘。我一直安慰自己同眯,他們只是感情好绽昼,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著须蜗,像睡著了一般硅确。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上明肮,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天菱农,我揣著相機(jī)與錄音,去河邊找鬼柿估。 笑死循未,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播的妖,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼绣檬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嫂粟?” 一聲冷哼從身側(cè)響起娇未,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎星虹,沒想到半個月后零抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡宽涌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年平夜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卸亮。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡忽妒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兼贸,到底是詐尸還是另有隱情锰扶,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布寝受,位于F島的核電站坷牛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏很澄。R本人自食惡果不足惜京闰,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甩苛。 院中可真熱鬧蹂楣,春花似錦、人聲如沸讯蒲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墨林。三九已至赁酝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旭等,已是汗流浹背酌呆。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留搔耕,地道東北人隙袁。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親菩收。 傳聞我的和親對象是個殘疾皇子梨睁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353