Java 執行緒池的建立過程分析

展翅而飛發表於2016-11-04

最近在改進專案的併發功能,但開發起來磕磕碰碰的。看了好多資料,總算加深了認識。於是打算配合檢視原始碼,總結併發程式設計的原理。

準備從用得最多的執行緒池開始,圍繞建立、執行、關閉認識執行緒池整個生命週期的實現原理。後續再研究原子變數、併發容器、阻塞佇列、同步工具、鎖等等主題。java.util.concurrent裡的併發工具用起來不難,但不能僅僅會用,我們要read the fucking source code,哈哈。順便說聲,我用的JDK是1.8。

Executor框架

Executor是一套執行緒池管理框架,介面裡只有一個方法execute,執行Runnable任務。ExecutorService介面擴充套件了Executor,新增了執行緒生命週期的管理,提供任務終止、返回任務結果等方法。AbstractExecutorService實現了ExecutorService,提供例如submit方法的預設實現邏輯。

然後到今天的主題ThreadPoolExecutor,繼承了AbstractExecutorService,提供執行緒池的具體實現。

構造方法

下面是ThreadPoolExecutor最普通的建構函式,最多有七個引數。具體程式碼不貼了,只是一些引數校驗和設定的語句。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    }

corePoolSize是執行緒池的目標大小,即是執行緒池剛剛建立起來,還沒有任務要執行時的大小。maximumPoolSize是執行緒池的最大上限。keepAliveTime是執行緒的存活時間,當執行緒池內的執行緒數量大於corePoolSize,超出存活時間的空閒執行緒就會被回收。unit就不用說了,剩下的三個引數看後文的分析。

預設的定製執行緒池

ThreadPoolExecutor預設了一些已經定製好的執行緒池,由Executors裡的工廠方法建立。下面分析newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool的建立引數。

newFixedThreadPool

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

newFixedThreadPool的corePoolSize和maximumPoolSize都設定為傳入的固定數量,keepAliveTim設定為0。執行緒池建立後,執行緒數量將會固定不變,適合需要執行緒很穩定的場合。

newSingleThreadExecutor

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

newSingleThreadExecutor是執行緒數量固定為1的newFixedThreadPool版本,保證池內的任務序列。注意到返回的是FinalizableDelegatedExecutorService,來看看原始碼:

static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
        FinalizableDelegatedExecutorService(ExecutorService executor) {
            super(executor);
        }
        protected void finalize() {
            super.shutdown();
        }
    }

FinalizableDelegatedExecutorService繼承了DelegatedExecutorService,僅僅在gc時增加關閉執行緒池的操作,再來看看DelegatedExecutorService的原始碼:

    static class DelegatedExecutorService extends AbstractExecutorService {
        private final ExecutorService e;
        DelegatedExecutorService(ExecutorService executor) { e = executor; }
        public void execute(Runnable command) { e.execute(command); }
        public void shutdown() { e.shutdown(); }
        public List<Runnable> shutdownNow() { return e.shutdownNow(); }
        public boolean isShutdown() { return e.isShutdown(); }
        public boolean isTerminated() { return e.isTerminated(); }
        //...
    }

程式碼很簡單,DelegatedExecutorService包裝了ExecutorService,使其只暴露出ExecutorService的方法,因此不能再配置執行緒池的引數。本來,執行緒池建立的引數是可以調整的,ThreadPoolExecutor提供了set方法。使用newSingleThreadExecutor目的是生成單執行緒序列的執行緒池,如果還能配置執行緒池大小,那就沒意思了。

Executors還提供了unconfigurableExecutorService方法,將普通執行緒池包裝成不可配置的執行緒池。如果不想執行緒池被不明所以的後人修改,可以呼叫這個方法。

newCachedThreadPool

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

newCachedThreadPool生成一個會快取的執行緒池,執行緒數量可以從0到Integer.MAX_VALUE,超時時間為1分鐘。執行緒池用起來的效果是:如果有空閒執行緒,會複用執行緒;如果沒有空閒執行緒,會新建執行緒;如果執行緒空閒超過1分鐘,將會被回收。

newScheduledThreadPool

newScheduledThreadPool將會建立一個可定時執行任務的執行緒池。這個不打算在本文展開,後續會另開文章細講。

等待佇列

newCachedThreadPool的執行緒上限幾乎等同於無限,但系統資源是有限的,任務的處理速度總有可能比不上任務的提交速度。因此,可以為ThreadPoolExecutor提供一個阻塞佇列來儲存因執行緒不足而等待的Runnable任務,這就是BlockingQueue。

JDK為BlockingQueue提供了幾種實現方式,常用的有:

  • ArrayBlockingQueue:陣列結構的阻塞佇列
  • LinkedBlockingQueue:連結串列結構的阻塞佇列
  • PriorityBlockingQueue:有優先順序的阻塞佇列
  • SynchronousQueue:不會儲存元素的阻塞佇列

newFixedThreadPool和newSingleThreadExecutor在預設情況下使用一個無界的LinkedBlockingQueue。要注意的是,如果任務一直提交,但執行緒池又不能及時處理,等待佇列將會無限制地加長,系統資源總會有消耗殆盡的一刻。所以,推薦使用有界的等待佇列,避免資源耗盡。但解決一個問題,又會帶來新問題:佇列填滿之後,再來新任務,這個時候怎麼辦?後文會介紹如何處理佇列飽和。

newCachedThreadPool使用的SynchronousQueue十分有趣,看名稱是個佇列,但它卻不能儲存元素。要將一個任務放進佇列,必須有另一個執行緒去接收這個任務,一個進就有一個出,佇列不會儲存任何東西。因此,SynchronousQueue是一種移交機制,不能算是佇列。newCachedThreadPool生成的是一個沒有上限的執行緒池,理論上提交多少任務都可以,使用SynchronousQueue作為等待佇列正合適。

飽和策略

當有界的等待佇列滿了之後,就需要用到飽和策略去處理,ThreadPoolExecutor的飽和策略通過傳入RejectedExecutionHandler來實現。如果沒有為建構函式傳入,將會使用預設的defaultHandler。

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
public static class AbortPolicy implements RejectedExecutionHandler {
       public AbortPolicy() { }
       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
           throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
       }
   }

AbortPolicy是預設的實現,直接丟擲一個RejectedExecutionException異常,讓呼叫者自己處理。除此之外,還有幾種飽和策略,來看一下:

   public static class DiscardPolicy implements RejectedExecutionHandler {
       public DiscardPolicy() { }
       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
       }
   }

DiscardPolicy的rejectedExecution直接是空方法,什麼也不幹。如果佇列滿了,後續的任務都拋棄掉。

   public static class DiscardOldestPolicy implements RejectedExecutionHandler {
       public DiscardOldestPolicy() { }
       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
           if (!e.isShutdown()) {
               e.getQueue().poll();
               e.execute(r);
           }
       }
   }

DiscardOldestPolicy會將等待佇列裡最舊的任務踢走,讓新任務得以執行。

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

最後一種飽和策略是CallerRunsPolicy,它既不拋棄新任務,也不拋棄舊任務,而是直接在當前執行緒執行這個任務。當前執行緒一般就是主執行緒啊,讓主執行緒執行任務,說不定就阻塞了。如果不是想清楚了整套方案,還是少用這種策略為妙。

ThreadFactory

每當執行緒池需要建立一個新執行緒,都是通過執行緒工廠獲取。如果不為ThreadPoolExecutor設定一個執行緒工廠,就會使用預設的defaultThreadFactory:

public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}
static class DefaultThreadFactory implements ThreadFactory {
       private static final AtomicInteger poolNumber = new AtomicInteger(1);
       private final ThreadGroup group;
       private final AtomicInteger threadNumber = new AtomicInteger(1);
       private final String namePrefix;

       DefaultThreadFactory() {
           SecurityManager s = System.getSecurityManager();
           group = (s != null) ? s.getThreadGroup() :
                                 Thread.currentThread().getThreadGroup();
           namePrefix = "pool-" +
                         poolNumber.getAndIncrement() +
                        "-thread-";
       }

       public Thread newThread(Runnable r) {
           Thread t = new Thread(group, r,
                                 namePrefix + threadNumber.getAndIncrement(),
                                 0);
           if (t.isDaemon())
               t.setDaemon(false);
           if (t.getPriority() != Thread.NORM_PRIORITY)
               t.setPriority(Thread.NORM_PRIORITY);
           return t;
       }
   }

平時列印執行緒池裡執行緒的name時,會輸出形如pool-1-thread-1之類的名稱,就是在這裡設定的。這個預設的執行緒工廠,建立的執行緒是普通的非守護執行緒,如果需要定製,實現ThreadFactory後傳給ThreadPoolExecutor即可。

不看程式碼不總結不會知道,光是執行緒池的建立就可以引出很多學問。別看平時建立執行緒池是一句程式碼的事,其實ThreadPoolExecutor提供了很靈活的定製方法。

歡迎留言和轉發,下一篇打算分析執行緒池如何執行任務

相關文章