Executors:為什麼阿里不待見我?

三分惡發表於2021-11-11

大家好,我是Excutors,一個老實的工具類。

有個叫老三的程式設計師在文章 要是以前有人這麼講執行緒池,我早就該明白了!裡挖了一個坑,說要把我介紹給大家認識認識。

我其實挺委屈的,作為一個沒得感情,老實幹活的工具類,我卻上了阿里巴巴的黑名單。他們在一本叫《Java開發手冊》的冊子裡寫道:

禁止使用Excutors

作者畫外音:人家為啥給你拉黑,不寫的清清楚楚嘛,你有啥可委屈的。而且你這個傢伙就是表面看起來老實,活是你乾的嗎?幹活的不都是小老弟ThreadPoolExecutor。來,我一個個給你數。

1. newFixedThreadPool

FixedThreadPool,是一個固定大小的執行緒池。

看一下它的原始碼實現:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

直接呼叫ThreadPoolExecutor的構造方法。

  • 核心執行緒數最大執行緒數相同
  • 使用LinkedBlockingQueue作為任務佇列

FixedThreadPoolexecute()執行示意圖:

FixedThreadPool

整體執行過程:

  • 當前執行執行緒少於corePoolSize,則建立新執行緒執行任務
  • 當前執行執行緒大於corePoolSize,將任務加入LinkedBlockingQueue
  • 執行緒池中執行緒執行完任務後,會迴圈從LinkedBlockingQueue中獲取任務執行

因為使用無界佇列LinkedBlockingQueue來儲存不能執行的任務,所以不會觸發拒絕服務策略,可能會導致OOM

2. newSingleThreadExecutor

SingleThreadExecutor是使用單個執行緒工作的執行緒池。

實現原始碼如下:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

直接呼叫ThreadPoolExecutor的構造方法。

  • 核心執行緒數最大執行緒數都是1
  • 使用LinkedBlockingQueue作為任務佇列

SingleThreadExecutor的執行流程:

SingleThreadExecutor執行流程

  • 當前無執行執行緒,建立一個執行緒來執行任務
  • 當前有執行緒執行,將任務加入LinkedBlockingQueue
  • 執行緒執行完任務後,會迴圈從LinkedBlockingQueue中獲取任務來執行

這裡用了無界佇列LinkedBlockingQueue,同樣可能會導致OOM

3. newCachedThreadPool

CachedThreadPool是一個會根據需要建立新執行緒的執行緒池。

實現原始碼:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

直接呼叫ThreadPoolExecutor的構造方法。

  • 核心執行緒數為0,最大執行緒數是非常大的一個數字Integer.MAX_VALUE
  • 使用沒有容量的SynchronousQueue作為工作佇列
  • keepAliveTime設定為60L,空閒執行緒空閒60秒之後就會被終止

CachedThreadPool的執行流程:

CachedThreadPool執行流程

  • 如果當前有空閒執行緒,使用空閒執行緒來執行任務
  • 如果沒有空閒執行緒,建立一個新執行緒來執行任務
  • 新建的執行緒執行完任務後,會執行poll(keepAliveTime,TimeUnit.NANOSECONDS),在SynchronousQueue裡等待60s

這裡執行緒池的大小沒有限制,可能會無限建立執行緒,導致OOM

4. newScheduledThreadPool

ScheduledThreadPool是一個具備排程功能的執行緒池。

實現原始碼:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

可以看到,這個執行緒池不太一樣,它呼叫的是ScheduledThreadPoolExecutor的構造方法。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  • 最大執行緒數是Integer.MAX_VALUE,無限大
  • 使用DelayedWorkQueue作為任務佇列

ScheduledThreadPoolExecutor執行任務的流程:

ScheduledThreadPool執行流程

主要分為兩大部分:

  1. 呼叫scheduleAtFixedRate()/scheduleWithFixedDelay()方法,會向DelayQueue新增一個ScheduledFutureTask
  2. 執行緒池的執行緒從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

它同樣可以無限建立執行緒,所以也存在OOM的風險。

為了實現週期性執行任務,ScheduledThreadPoolExecutorThreadPoolExecutor進行了一些改造[4]:

  • ScheduledFutureTask來作為排程任務的實現

    它主要包含了3個成員變數time(任務將要被執行的具體時間)sequenceNumber(任務的序號)period(任務執行的間隔週期)

  • 使用DelayQueue作為任務佇列

    DelayQueue封裝了了一個PriorityQueue,會對對佇列中的ScheduledFutureTask進行排序,排序的優先順序time>sequenceNumber。

ScheduledThreadPoolExecutor執行流程

ScheduledThreadPoolExecutor的任務執行主要分為4步:

  1. 執行緒池裡的執行緒1DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())
  2. 執行緒1執行這個ScheduledFutureTask
  3. 執行緒1修改ScheduledFutureTasktime變數為下次將要被執行的時間。
  4. 執行緒1把這個修改time之後的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())

Excutors自述:這,這……工具類出的問題不叫bug。雖然我偷懶不幹活,還可能會OOM,但我還是一個好工具類,嗚嗚……

作者:是啊,其實Excutors有什麼錯呢?它只是一個沒得感情的工具類,有錯的只是不恰當地用它的人。所以,知其然且知其所以然,搞懂原理,靈活應用。我們應該像一個士兵一樣,不只是會扣動扳機,還會拆解保養槍械。

我是三分惡,一個號稱能文能武的全棧開發。

點贊關注不迷路,我們們下期見!



參考:

[1]. 《Java併發程式設計的藝術》

[2]. 講真 這次絕對讓你輕鬆學習執行緒池

[3]. 小傅哥 《Java面經手冊》

[4]. 《Java併發程式設計之美》

[5]. 阿里巴巴《Java開發手冊》

相關文章