線程池你真不來了解一下嗎?

前言

只有光頭才能變強

回顧前面:java

本篇主要是講解線程池,這是我在多線程的倒數第二篇了,後面還會有一篇死鎖。主要將多線程的基礎過一遍,之後有機會再繼續深刻算法

那麼接下來就開始吧,若是文章有錯誤的地方請你們多多包涵,不吝在評論區指正哦~編程

聲明:本文使用JDK1.8

1、線程池簡介

線程池能夠看作是線程的集合。在沒有任務時線程處於空閒狀態,當請求到來:線程池給這個請求分配一個空閒的線程,任務完成後回到線程池中等待下次任務(而不是銷燬)。這樣就實現了線程的重用c#

咱們來看看若是沒有使用線程池的狀況是這樣的:api

  • 爲每一個請求都新開一個線程
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
    }
}

爲每一個請求都開一個新的線程雖然理論上是能夠的,可是會有缺點微信

  • 線程生命週期的開銷很是高。每一個線程都有本身的生命週期,建立和銷燬線程所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,而且還會有某些空閒線程也會佔用資源
  • 程序的穩定性和健壯性會降低,每一個請求開一個線程。若是受到了惡意攻擊或者請求過多(內存不足),程序很容易就奔潰掉了。

因此說:咱們的線程最好是交由線程池來管理,這樣能夠減小對線程生命週期的管理,必定程度上提升性能。多線程

2、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;
    }

}

執行完任務以後能夠獲取獲得任務返回的數據

3、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. 任務拒絕策略

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

線程數量要點

  • 若是運行線程的數量少於核心線程數量,則建立新的線程處理請求
  • 若是運行線程的數量大於核心線程數量,小於最大線程數量,則當隊列滿的時候才建立新的線程
  • 若是核心線程數量等於最大線程數量,那麼將建立固定大小的鏈接池
  • 若是設置了最大線程數量爲無窮,那麼容許線程池適合任意的併發數量

線程空閒時間要點:

  • 當前線程數大於核心線程數,若是空閒時間已經超過了,那該線程會銷燬

排隊策略要點

  • 同步移交:不會放到隊列中,而是等待線程執行它。若是當前線程沒有執行,極可能會新開一個線程執行。
  • 無界限策略:若是核心線程都在工做,該線程會放到隊列中。因此線程數不會超過核心線程數
  • 有界限策略:能夠避免資源耗盡,可是必定程度上減低了吞吐量

當線程關閉或者線程數量滿了和隊列飽和了,就有拒絕任務的狀況了:

拒絕任務策略:

  • 直接拋出異常
  • 使用調用者的線程來處理
  • 直接丟掉這個任務
  • 丟掉最老的任務

4、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);
    }

5、線程池關閉

ThreadPoolExecutor提供了shutdown()shutdownNow()兩個方法來關閉線程池

shutdown() :

shutdownNow():

區別:

  • 調用shutdown()後,線程池狀態馬上變爲SHUTDOWN,而調用shutdownNow(),線程池狀態馬上變爲STOP
  • shutdown()等待任務執行完才中斷線程,而shutdownNow()不等任務執行完就中斷了線程。

6、總結

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

明天但願能夠把死鎖寫出來,敬請期待~~~

還有剩下的幾個線程池(給出了參考資料):

參考資料:

若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠 關注微信公衆號:Java3y。爲了你們方便,剛新建了一下 qq羣:742919422,你們也能夠去交流交流。謝謝支持了!但願能多介紹給其餘有須要的朋友

文章的目錄導航

相關文章
相關標籤/搜索