java多執行緒系列:Executors框架

雲梟發表於2018-06-12

目錄

  1. Executor介面介紹
  2. ExecutorService常用介面介紹
  3. 建立執行緒池的一些方法介紹
  4. 疑問解答

Executor介面介紹

Executor是一個介面,裡面提供了一個execute方法,該方法接收一個Runable引數,如下

public interface Executor {
    void execute(Runnable command);
}
複製程式碼

Executor框架的常用類和介面結構圖

Executor框架的常用類和介面結構圖

執行緒物件及執行緒執行返回的物件

執行緒物件及執行緒執行返回的物件

執行緒物件

執行緒物件就是提交給執行緒池的任務,可以實現Runable介面或Callable介面。或許這邊會產生一個疑問,為什麼Runable介面和Callable介面沒有任何關聯,卻都能作為任務來執行?大家可以思考下,文章的結尾會對此進行說明

Future介面

Future介面和FutureTask類是用來接收執行緒非同步執行後返回的結果,可以看到下方ExecutorService介面的submit方法返回的就是Future。

ExecutorService常用介面介紹

接下來我們來看看繼承了Executor介面的ExecutorService

public interface ExecutorService extends Executor {
    //正常關閉(不再接收新任務,執行完佇列中的任務)
    void shutdown();
	//強行關閉(關閉當前正在執行的任務,返回所有尚未啟動的任務清單)
    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);
	...
}
複製程式碼

ThreadPoolExecutor建構函式介紹

在介紹穿件執行緒池的方法之前要先介紹一個類ThreadPoolExecutor,應為Executors工廠大部分方法都是返回ThreadPoolExecutor物件,先來看看它的建構函式吧

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {...}
複製程式碼

引數介紹

引數 型別 含義
corePoolSize int 核心執行緒數
maximumPoolSize int 最大執行緒數
keepAliveTime long 存活時間
unit TimeUnit 時間單位
workQueue BlockingQueue 存放執行緒的佇列
threadFactory ThreadFactory 建立執行緒的工廠
handler RejectedExecutionHandler 多餘的的執行緒處理器(拒絕策略)

建立執行緒池的一些方法介紹

為什麼要講ExecutorService介面呢?是因為我們使用Executors的方法時返回的大部分都是ExecutorService。 Executors提供了幾個建立執行緒池方法,接下來我就介紹一下這些方法

newFixedThreadPool(int nThreads)
建立一個執行緒的執行緒池,若空閒則執行,若沒有空閒執行緒則暫緩在任務佇列中。

newWorkStealingPool()
建立持有足夠執行緒的執行緒池來支援給定的並行級別,並通過使用多個佇列,減少競爭,它需要穿一個並行級別的引數,如果不傳,則被設定為預設的CPU數量。

newSingleThreadExecutor()
該方法返回一個固定數量的執行緒池  
該方法的執行緒始終不變,當有一個任務提交時,若執行緒池空閒,則立即執行,若沒有,則會被暫緩在一個任務佇列只能怪等待有空閒的執行緒去執行。

newCachedThreadPool() 
返回一個可根據實際情況調整執行緒個數的執行緒池,不限制最大執行緒數量,若有空閒的執行緒則執行任務,若無任務則不建立執行緒,並且每一個空閒執行緒會在60秒後自動回收。

newScheduledThreadPool(int corePoolSize)
返回一個SchededExecutorService物件,但該執行緒池可以設定執行緒的數量,支援定時及週期性任務執行。
 
newSingleThreadScheduledExecutor()
建立一個單例執行緒池,定期或延時執行任務。  
 
複製程式碼

下面講解下幾個常用的方法,建立單個的就不說明了

newFixedThreadPool方法

該方法建立指定執行緒數量的執行緒池,沒有限制可存放的執行緒數量(無界佇列),適用於執行緒任務執行較快的場景。

FixedThreadPool的execute()的執行示意圖

看看Executors工廠內部是如何實現的

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

可以看到返回的是一個ThreadPoolExecutor物件,核心執行緒數和是最大執行緒數都是傳入的引數,存活時間是0,時間單位是毫秒,阻塞佇列是無界佇列LinkedBlockingQueue。

由於佇列採用的是無界佇列LinkedBlockingQueue,最大執行緒數maximumPoolSize和keepAliveTime都是無效引數,拒絕策略也將無效,為什麼?

這裡又延伸出一個問題,無界佇列說明任務沒有上限,如果執行的任務比較耗時,那麼新的任務會一直存放線上程池中,執行緒池的任務會越來越多,將會導致什麼後果?下面的程式碼可以試試

public class Main {

    public static void main(String[] args){
        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        while (true){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

    }
}
複製程式碼

示例程式碼

public class Main {

    public static void main(String[] args){
        ExecutorService pool = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 8; i++) {
            int finalI = i + 1;
            pool.submit(() -> {
                try {
                    System.out.println("任務"+ finalI +":開始等待2秒,時間:"+LocalTime.now()+",當前執行緒名:"+Thread.currentThread().getName());
                    Thread.sleep(2000);
                    System.out.println("任務"+ finalI +":結束等待2秒,時間:"+LocalTime.now()+",當前執行緒名:"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

        }
        pool.shutdown();
    }
}
複製程式碼

輸出結果

任務4:開始等待2秒,時間:17:13:22.048,當前執行緒名:pool-1-thread-4
任務2:開始等待2秒,時間:17:13:22.048,當前執行緒名:pool-1-thread-2
任務3:開始等待2秒,時間:17:13:22.048,當前執行緒名:pool-1-thread-3
任務1:開始等待2秒,時間:17:13:22.048,當前執行緒名:pool-1-thread-1

任務2:結束等待2秒,時間:17:13:24.048,當前執行緒名:pool-1-thread-2
任務3:結束等待2秒,時間:17:13:24.048,當前執行緒名:pool-1-thread-3
任務1:結束等待2秒,時間:17:13:24.048,當前執行緒名:pool-1-thread-1
任務4:結束等待2秒,時間:17:13:24.048,當前執行緒名:pool-1-thread-4
任務6:開始等待2秒,時間:17:13:24.049,當前執行緒名:pool-1-thread-4
任務7:開始等待2秒,時間:17:13:24.049,當前執行緒名:pool-1-thread-1
任務5:開始等待2秒,時間:17:13:24.049,當前執行緒名:pool-1-thread-3
任務8:開始等待2秒,時間:17:13:24.049,當前執行緒名:pool-1-thread-2

任務5:結束等待2秒,時間:17:13:26.050,當前執行緒名:pool-1-thread-3
任務7:結束等待2秒,時間:17:13:26.050,當前執行緒名:pool-1-thread-1
任務8:結束等待2秒,時間:17:13:26.051,當前執行緒名:pool-1-thread-2
任務6:結束等待2秒,時間:17:13:26.050,當前執行緒名:pool-1-thread-4
複製程式碼

可以看出任務1-4在同一時間執行,在2秒後執行完畢,同時開始執行任務5-8。說明方法內部只建立了4個執行緒,其他任務存放在佇列中等待執行。

newCachedThreadPool方法

newCachedThreadPool方法建立的執行緒池會根據需要自動建立新執行緒。

CachedThreadPool的execute()的執行示意圖

看看Executors工廠內部是如何實現的

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

newCachedThreadPool方法也是返回ThreadPoolExecutor物件,核心執行緒是0,最大執行緒數是Integer的最MAX_VALUE,存活時間是60,時間單位是秒,SynchronousQueue佇列。

從傳入的引數可以得知,在newCachedThreadPool方法中的空閒執行緒存活時間時60秒,一旦超過60秒執行緒就會被終止。這邊還隱含了一個問題,如果執行的執行緒較慢,而提交任務的速度快於執行緒執行的速度,那麼就會不斷的建立新的執行緒,從而導致cpu和記憶體的增長。

程式碼和newFixedThreadPool一樣迴圈新增新的執行緒任務,我的電腦執行就會出現如下錯誤

An unrecoverable stack overflow has occurred.

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:714)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at com.learnConcurrency.executor.cachedThreadPool.Main.main(Main.java:11)
Process finished with exit code -1073741571 (0xC00000FD)

複製程式碼

關於SynchronousQueue佇列,它是一個沒有容量的阻塞佇列,任務傳遞的示意圖如下

CachedThreadPool的任務傳遞示意圖

示例程式碼

public class Main {
    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 8; i++) {
            int finalI = i + 1;
            pool.submit(() -> {
                try {
                    System.out.println("任務"+ finalI +":開始等待60秒,時間:"+LocalTime.now()+",當前執行緒名:"+Thread.currentThread().getName());
                    Thread.sleep(60000);
                    System.out.println("任務"+ finalI +":結束等待60秒,時間:"+LocalTime.now()+",當前執行緒名:"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            //睡眠10秒
            Thread.sleep(10000);
        }
        pool.shutdown();
    }
}
複製程式碼

執行結果

任務1:開始等待60秒,時間:17:15:21.570,當前執行緒名:pool-1-thread-1
任務2:開始等待60秒,時間:17:15:31.553,當前執行緒名:pool-1-thread-2
任務3:開始等待60秒,時間:17:15:41.555,當前執行緒名:pool-1-thread-3
任務4:開始等待60秒,時間:17:15:51.554,當前執行緒名:pool-1-thread-4
任務5:開始等待60秒,時間:17:16:01.554,當前執行緒名:pool-1-thread-5
任務6:開始等待60秒,時間:17:16:11.555,當前執行緒名:pool-1-thread-6
任務7:開始等待60秒,時間:17:16:21.555,當前執行緒名:pool-1-thread-7
任務1:結束等待60秒,時間:17:16:21.570,當前執行緒名:pool-1-thread-1
任務2:結束等待60秒,時間:17:16:31.554,當前執行緒名:pool-1-thread-2

任務8:開始等待60秒,時間:17:16:31.556,當前執行緒名:pool-1-thread-2
任務3:結束等待60秒,時間:17:16:41.555,當前執行緒名:pool-1-thread-3
任務4:結束等待60秒,時間:17:16:51.556,當前執行緒名:pool-1-thread-4
任務5:結束等待60秒,時間:17:17:01.556,當前執行緒名:pool-1-thread-5
任務6:結束等待60秒,時間:17:17:11.555,當前執行緒名:pool-1-thread-6
任務7:結束等待60秒,時間:17:17:21.556,當前執行緒名:pool-1-thread-7
任務8:結束等待60秒,時間:17:17:31.557,當前執行緒名:pool-1-thread-2
複製程式碼

示例程式碼中每個任務都睡眠60秒,每次迴圈新增任務睡眠10秒,從執行結果來看,新增的7個任務都是由不同的執行緒來執行,而此時執行緒1和2都執行完畢,任務8新增進來由之前建立的pool-1-thread-2執行。

newScheduledThreadPool方法

這個執行緒池主要用來延遲執行任務或者定期執行任務。

看看Executors工廠內部是如何實現的

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
複製程式碼

這裡返回的是ScheduledThreadPoolExecutor物件,我們繼續深入進去看看

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
複製程式碼

這裡呼叫的是父類的建構函式,ScheduledThreadPoolExecutor的父類是ThreadPoolExecutor,所以返回的也是ThreadPoolExecutor物件。核心執行緒數是傳入的引數corePoolSize,執行緒最大值是Integer的MAX_VALUE,存活時間時0,時間單位是納秒,佇列是DelayedWorkQueue。

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {}
複製程式碼

下面是ScheduledExecutorService的一些方法

public interface ScheduledExecutorService extends ExecutorService {
	//delay延遲時間,unit延遲單位,只執行1次,在經過delay延遲時間之後開始執行
    public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
	//首次執行時間時然後在initialDelay之後,然後在initialDelay+period 後執行,接著在 initialDelay + 2 * period 後執行,依此類推
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
	//首次執行時間時然後在initialDelay之後,然後延遲delay時間執行
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
}
複製程式碼

疑問解答

Runable介面和Callable介面

那麼就從提交任務入口看看吧

submit方法是由抽象類AbstractExecutorService實現的

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
複製程式碼

可以看出將傳入的Runnable物件和Callable傳入一個newTaskFor方法,然後返回一個RunnableFuture物件

我們再來看看newTaskFor方法

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}
複製程式碼

這裡都是呼叫FutureTask的建構函式,我們接著往下看

private Callable<V> callable;

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;      
}

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       
}
複製程式碼

FutureTask類中有個成員變數callable,而傳入的Runnable物件則繼續呼叫Executors工廠類的callable方法返回一個Callable物件

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}
//介面卡
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}
複製程式碼

好了,到這裡也就真相大白了,Runnable物件經過一系列的方法呼叫,最終被RunnableAdapter介面卡適配成Callable物件。方法呼叫圖如下

方法呼叫圖

GitHub地址

地址在這

覺得不錯的點個star

下一篇會介紹下自定義執行緒池,後續也會更新newWorkStealingPool方法介紹

參考資料

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

[2] Java 併發程式設計實戰

相關文章