執行緒池你真不來了解一下嗎?

Java3y發表於2018-05-05

前言

只有光頭才能變強

回顧前面:

本篇主要是講解執行緒池,這是我在多執行緒的倒數第二篇了,後面還會有一篇死鎖。主要將多執行緒的基礎過一遍,以後有機會再繼續深入

那麼接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~

宣告:本文使用JDK1.8

一、執行緒池簡介

執行緒池可以看做是執行緒的集合。在沒有任務時執行緒處於空閒狀態,當請求到來:執行緒池給這個請求分配一個空閒的執行緒,任務完成後回到執行緒池中等待下次任務**(而不是銷燬)。這樣就實現了執行緒的重用**。

我們來看看如果沒有使用執行緒池的情況是這樣的:

  • 為每個請求都新開一個執行緒

public class ThreadPerTaskWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
			// 為每個請求都建立一個新的執行緒
            final Socket connection = socket.accept();
            Runnable task = () -> handleRequest(connection);
            new Thread(task).start();
        }
    }
    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}
複製程式碼

為每個請求都開一個新的執行緒雖然理論上是可以的,但是會有缺點

  • 執行緒生命週期的開銷非常高。每個執行緒都有自己的生命週期,建立和銷燬執行緒所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,並且還會有某些空閒執行緒也會佔用資源
  • 程式的穩定性和健壯性會下降,每個請求開一個執行緒。如果受到了惡意攻擊或者請求過多(記憶體不足),程式很容易就奔潰掉了。

所以說:我們的執行緒最好是交由執行緒池來管理,這樣可以減少對執行緒生命週期的管理,一定程度上提高效能。

二、JDK提供的執行緒池API

JDK給我們提供了Excutor框架來使用執行緒池,它是執行緒池的基礎

  • Executor提供了一種將**“任務提交”與“任務執行”**分離開來的機制(解耦)

下面我們來看看JDK執行緒池的總體api架構:

執行緒池你真不來了解一下嗎?

接下來我們把這些API都過一遍看看:

Executor介面:

執行緒池你真不來了解一下嗎?

ExcutorService介面:

執行緒池你真不來了解一下嗎?

AbstractExecutorService類:

執行緒池你真不來了解一下嗎?

ScheduledExecutorService介面:

執行緒池你真不來了解一下嗎?

ThreadPoolExecutor類:

執行緒池你真不來了解一下嗎?

ScheduledThreadPoolExecutor類:

執行緒池你真不來了解一下嗎?

2.1ForkJoinPool執行緒池

除了ScheduledThreadPoolExecutor和ThreadPoolExecutor類執行緒池以外,還有一個是JDK1.7新增的執行緒池:ForkJoinPool執行緒池

於是我們的類圖就可以變得完整一些:

執行緒池你真不來了解一下嗎?

JDK1.7中新增的一個執行緒池,與ThreadPoolExecutor一樣,同樣繼承了AbstractExecutorService。ForkJoinPool是Fork/Join框架的兩大核心類之一。與其它型別的ExecutorService相比,其主要的不同在於採用了工作竊取演算法(work-stealing):所有池中執行緒會嘗試找到並執行已被提交到池中的或由其他執行緒建立的任務。這樣很少有執行緒會處於空閒狀態,非常高效。這使得能夠有效地處理以下情景:大多數由任務產生大量子任務的情況;從外部客戶端大量提交小任務到池中的情況。

來源:

2.2補充:Callable和Future

學到了執行緒池,我們可以很容易地發現:很多的API都有Callable和Future這麼兩個東西。


	Future<?> submit(Runnable task)
	<T> Future<T> submit(Callable<T> task)
複製程式碼

其實它們也不是什麼高深的東西~~~

我們可以簡單認為:Callable就是Runnable的擴充套件

  • Runnable沒有返回值,不能丟擲受檢查的異常,而Callable可以

執行緒池你真不來了解一下嗎?

也就是說:當我們的任務需要返回值的時,我們就可以使用Callable!

Future一般我們認為是Callable的返回值,但他其實代表的是任務的生命週期(當然了,它是能獲取得到Callable的返回值的)

執行緒池你真不來了解一下嗎?

簡單來看一下他們的用法:


public class CallableDemo {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 建立執行緒池物件
		ExecutorService pool = Executors.newFixedThreadPool(2);

		// 可以執行Runnable物件或者Callable物件代表的執行緒
		Future<Integer> f1 = pool.submit(new MyCallable(100));
		Future<Integer> f2 = pool.submit(new MyCallable(200));

		// V get()
		Integer i1 = f1.get();
		Integer i2 = f2.get();

		System.out.println(i1);
		System.out.println(i2);

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

Callable任務:


public class MyCallable implements Callable<Integer> {

	private int number;

	public MyCallable(int number) {
		this.number = number;
	}

	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for (int x = 1; x <= number; x++) {
			sum += x;
		}
		return sum;
	}

}

複製程式碼

執行完任務之後可以獲取得到任務返回的資料

執行緒池你真不來了解一下嗎?

三、ThreadPoolExecutor詳解

這是用得最多的執行緒池,所以本文會重點講解它。

我們來看看頂部註釋:

執行緒池你真不來了解一下嗎?

3.1內部狀態

執行緒池你真不來了解一下嗎?

變數ctl定義為AtomicInteger,記錄了“執行緒池中的任務數量”和“執行緒池的狀態”兩個資訊

執行緒池你真不來了解一下嗎?

執行緒的狀態:

  • RUNNING:執行緒池能夠接受新任務,以及對新新增的任務進行處理。
  • SHUTDOWN:執行緒池不可以接受新任務,但是可以對已新增的任務進行處理。
  • STOP:執行緒池不接收新任務,不處理已新增的任務,並且會中斷正在處理的任務
  • TIDYING:當所有的任務已終止,ctl記錄的"任務數量"為0,執行緒池會變為TIDYING狀態。當執行緒池變為TIDYING狀態時,會執行鉤子函式terminated()。terminated()在ThreadPoolExecutor類中是空的,若使用者想線上程池變為TIDYING時,進行相應的處理;可以通過過載terminated()函式來實現。
  • TERMINATED:執行緒池徹底終止的狀態

執行緒池你真不來了解一下嗎?

各個狀態之間轉換:

執行緒池你真不來了解一下嗎?

3.2已預設實現的池

下面我就列舉三個比較常見的實現池:

  • newFixedThreadPool
  • newCachedThreadPool
  • SingleThreadExecutor

如果讀懂了上面對應的策略呀,執行緒數量這些,應該就不會太難看懂了。

3.2.1newFixedThreadPool

一個固定執行緒數的執行緒池,它將返回一個corePoolSize和maximumPoolSize相等的執行緒池


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

3.2.2newCachedThreadPool

非常有彈性的執行緒池,對於新的任務,如果此時執行緒池裡沒有空閒執行緒,執行緒池會毫不猶豫的建立一條新的執行緒去處理這個任務


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

3.2.3SingleThreadExecutor

使用單個worker執行緒的Executor


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

3.3構造方法

我們讀完上面的預設實現池還有對應的屬性,再回到構造方法看看

  • 構造方法可以讓我們自定義(擴充套件)執行緒池

    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;
    }
複製程式碼
  1. 指定核心執行緒數量
  2. 指定最大執行緒數量
  3. 允許執行緒空閒時間
  4. 時間物件
  5. 阻塞佇列
  6. 執行緒工廠
  7. 任務拒絕策略

再總結一遍這些引數的要點:

執行緒數量要點

  • 如果執行執行緒的數量少於核心執行緒數量,則建立新的執行緒處理請求
  • 如果執行執行緒的數量大於核心執行緒數量,小於最大執行緒數量,則當佇列滿的時候才建立新的執行緒
  • 如果核心執行緒數量等於最大執行緒數量,那麼將建立固定大小的連線池
  • 如果設定了最大執行緒數量為無窮,那麼允許執行緒池適合任意的併發數量

執行緒空閒時間要點:

  • 當前執行緒數大於核心執行緒數,如果空閒時間已經超過了,那該執行緒會銷燬

排隊策略要點

  • 同步移交:不會放到佇列中,而是等待執行緒執行它。如果當前執行緒沒有執行,很可能會新開一個執行緒執行。
  • 無界限策略:如果核心執行緒都在工作,該執行緒會放到佇列中。所以執行緒數不會超過核心執行緒數
  • 有界限策略:可以避免資源耗盡,但是一定程度上減低了吞吐量

當執行緒關閉或者執行緒數量滿了和佇列飽和了,就有拒絕任務的情況了:

拒絕任務策略:

  • 直接丟擲異常
  • 使用呼叫者的執行緒來處理
  • 直接丟掉這個任務
  • 丟掉最老的任務

四、execute執行方法

execute執行方法分了三步,以註釋的方式寫在程式碼上了~


    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
		//如果執行緒池中執行的執行緒數量<corePoolSize,則建立新執行緒來處理請求,即使其他輔助執行緒是空閒的。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

		//如果執行緒池中執行的執行緒數量>=corePoolSize,且執行緒池處於RUNNING狀態,且把提交的任務成功放入阻塞佇列中,就再次檢查執行緒池的狀態,
			// 1.如果執行緒池不是RUNNING狀態,且成功從阻塞佇列中刪除任務,則該任務由當前 RejectedExecutionHandler 處理。
			// 2.否則如果執行緒池中執行的執行緒數量為0,則通過addWorker(null, false)嘗試新建一個執行緒,新建執行緒對應的任務為null。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
		// 如果以上兩種case不成立,即沒能將任務成功放入阻塞佇列中,且addWoker新建執行緒失敗,則該任務由當前 RejectedExecutionHandler 處理。
        else if (!addWorker(command, false))
            reject(command);
    }
複製程式碼

五、執行緒池關閉

ThreadPoolExecutor提供了shutdown()shutdownNow()兩個方法來關閉執行緒池

shutdown() :

執行緒池你真不來了解一下嗎?

shutdownNow():

執行緒池你真不來了解一下嗎?

區別:

  • 呼叫shutdown()後,執行緒池狀態立刻變為SHUTDOWN,而呼叫shutdownNow(),執行緒池狀態立刻變為STOP
  • shutdown()等待任務執行完才中斷執行緒,而shutdownNow()不等任務執行完就中斷了執行緒。

六、總結

本篇博文主要簡單地將多執行緒的結構體系過了一篇,講了最常用的ThreadPoolExecutor執行緒池是怎麼使用的~~~

明天希望可以把死鎖寫出來,敬請期待~~~

還有剩下的幾個執行緒池(給出了參考資料):

參考資料:

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y

文章的目錄導航

相關文章