Java多線程:還不懂線程池嗎?一文帶你徹底搞懂!

熟悉Java多線程編程的同學都知道,當我們線程創建過多時,容易引發內存溢出,因此我們就有必要使用線程池的技術了。
最近看了一些相關文章,並親自研究了一下源碼,發現有些文章還是有些問題的,所以我也總結了一下,在此奉獻給大家。

目錄

1 線程池的優勢

總體來說,線程池有如下的優勢:
(1)降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
(2)提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
(3)提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

2 線程池的使用

線程池的真正實現類是ThreadPoolExecutor,其構造方法有如下4種:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

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

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到,其需要如下幾個參數:

  • corePoolSize(必需):核心線程數。默認情況下,核心線程會一直存活,但是當將allowCoreThreadTimeout設置爲true時,核心線程也會超時回收。
  • maximumPoolSize(必需):線程池所能容納的最大線程數。當活躍線程數達到該數值後,後續的新任務將會阻塞。
  • keepAliveTime(必需):線程閒置超時時長。如果超過該時長,非核心線程就會被回收。如果將allowCoreThreadTimeout設置爲true時,核心線程也會超時回收。
  • unit(必需):指定keepAliveTime參數的時間單位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任務隊列。通過線程池的execute()方法提交的Runnable對象將存儲在該參數中。其採用阻塞隊列實現。
  • threadFactory(可選):線程工廠。用於指定爲線程池創建新線程的方式。
  • handler(可選):拒絕策略。當達到最大線程數時需要執行的飽和策略。

線程池的使用流程如下:

// 創建線程池
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向線程池提交任務
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 線程執行的任務
    }
});
// 關閉線程池
threadPool.shutdown(); // 設置線程池的狀態爲SHUTDOWN,然後中斷所有沒有正在執行任務的線程
threadPool.shutdownNow(); // 設置線程池的狀態爲 STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表

3 線程池的工作原理

下面來描述一下線程池工作的原理,同時對上面的參數有一個更深的瞭解。其工作原理流程圖如下:

工作原理

通過上圖,相信大家已經對所有參數有個瞭解了。其實還有一點,在線程池中並沒有區分線程是否是核心線程的。下面我們再對任務隊列、線程工廠和拒絕策略做更多的說明。

4 線程池的參數

4.1 任務隊列(workQueue)

任務隊列是基於阻塞隊列實現的,即採用生產者消費者模式,在Java中需要實現BlockingQueue接口。但Java已經爲我們提供了7種阻塞隊列的實現:

  1. ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列(數組結構可配合指針實現一個環形隊列)。
  2. LinkedBlockingQueue: 一個由鏈表結構組成的有界阻塞隊列,在未指明容量時,容量默認爲Integer.MAX_VALUE
  3. PriorityBlockingQueue: 一個支持優先級排序的無界阻塞隊列,對元素沒有要求,可以實現Comparable接口也可以提供Comparator來對隊列中的元素進行比較。跟時間沒有任何關係,僅僅是按照優先級取任務。
  4. DelayQueue:類似於PriorityBlockingQueue,是二叉堆實現的無界優先級阻塞隊列。要求元素都實現Delayed接口,通過執行時延從隊列中提取任務,時間沒到任務取不出來。
  5. SynchronousQueue: 一個不存儲元素的阻塞隊列,消費者線程調用take()方法的時候就會發生阻塞,直到有一個生產者線程生產了一個元素,消費者線程就可以拿到這個元素並返回;生產者線程調用put()方法的時候也會發生阻塞,直到有一個消費者線程消費了一個元素,生產者纔會返回。
  6. LinkedBlockingDeque: 使用雙向隊列實現的有界雙端阻塞隊列。雙端意味着可以像普通隊列一樣FIFO(先進先出),也可以像棧一樣FILO(先進後出)。
  7. LinkedTransferQueue: 它是ConcurrentLinkedQueueLinkedBlockingQueueSynchronousQueue的結合體,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行爲一致,但是是無界的阻塞隊列。

注意有界隊列和無界隊列的區別:如果使用有界隊列,當隊列飽和時並超過最大線程數時就會執行拒絕策略;而如果使用無界隊列,因爲任務隊列永遠都可以添加任務,所以設置maximumPoolSize沒有任何意義。

4.2 線程工廠(threadFactory)

線程工廠指定創建線程的方式,需要實現ThreadFactory接口,並實現newThread(Runnable r)方法。該參數可以不用指定,Executors框架已經爲我們實現了一個默認的線程工廠:

/**
 * The default thread factory.
 */
private 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;
    }
}

4.3 拒絕策略(handler)

當線程池的線程數達到最大線程數時,需要執行拒絕策略。拒絕策略需要實現RejectedExecutionHandler接口,並實現rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不過Executors框架已經爲我們實現了4種拒絕策略:

  1. AbortPolicy(默認):丟棄任務並拋出RejectedExecutionException異常。
  2. CallerRunsPolicy:由調用線程處理該任務。
  3. DiscardPolicy:丟棄任務,但是不拋出異常。可以配合這種模式進行自定義的處理方式。
  4. DiscardOldestPolicy:丟棄隊列最早的未處理任務,然後重新嘗試執行任務。

5 功能線程池

嫌上面使用線程池的方法太麻煩?其實Executors已經爲我們封裝好了4種常見的功能線程池,如下:

  • 定長線程池(FixedThreadPool)
  • 定時線程池(ScheduledThreadPool )
  • 可緩存線程池(CachedThreadPool)
  • 單線程化線程池(SingleThreadExecutor)

5.1 定長線程池(FixedThreadPool)

創建方法的源碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • 特點:只有核心線程,線程數量固定,執行完立即回收,任務隊列爲鏈表結構的有界隊列。
  • 應用場景:控制線程最大併發數。

使用示例:

// 1. 創建定長線程池對象 & 設置線程池線程數量固定爲3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run() {
     System.out.println("執行任務啦");
  }
};
// 3. 向線程池提交任務
fixedThreadPool.execute(task);

5.2 定時線程池(ScheduledThreadPool )

創建方法的源碼:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
  • 特點:核心線程數量固定,非核心線程數量無限,執行完閒置10ms後回收,任務隊列爲延時阻塞隊列。
  • 應用場景:執行定時或週期性的任務。

使用示例:

// 1. 創建 定時線程池對象 & 設置線程池線程數量固定爲5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run() {
     System.out.println("執行任務啦");
  }
};
// 3. 向線程池提交任務
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延遲1s後執行任務
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延遲10ms後、每隔1000ms執行任務

5.3 可緩存線程池(CachedThreadPool)

創建方法的源碼:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • 特點:無核心線程,非核心線程數量無限,執行完閒置60s後回收,任務隊列爲不存儲元素的阻塞隊列。
  • 應用場景:執行大量、耗時少的任務。

使用示例:

// 1. 創建可緩存線程池對象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run() {
     System.out.println("執行任務啦");
  }
};
// 3. 向線程池提交任務
cachedThreadPool.execute(task);

5.4 單線程化線程池(SingleThreadExecutor)

創建方法的源碼:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • 特點:只有1個核心線程,無非核心線程,執行完立即回收,任務隊列爲鏈表結構的有界隊列。
  • 應用場景:不適合併發但可能引起IO阻塞性及影響UI線程響應的操作,如數據庫操作、文件操作等。

使用示例:

// 1. 創建單線程化線程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
  public void run() {
     System.out.println("執行任務啦");
  }
};
// 3. 向線程池提交任務
singleThreadExecutor.execute(task);

5.5 對比

對比 - 引自Carson_Ho

6 總結

Executors的4個功能線程池雖然方便,但現在已經不建議使用了,而是建議直接通過使用ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

其實Executors的4個功能線程有如下弊端:

  • FixedThreadPoolSingleThreadExecutor:主要問題是堆積的請求處理隊列均採用LinkedBlockingQueue,可能會耗費非常大的內存,甚至OOM。
  • CachedThreadPoolScheduledThreadPool:主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

更多的技術分享,盡在我的公衆號:Java小朔哥

還有我把近一年經歷過的面試,和一些刷過的面試題都做成了PDF,PDF都是可以免費分享給大家的,只要關注我的wx公衆號:java小朔哥,就可以獲取免費領取方式!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章