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

大家好,我是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開發手冊》