Java面試知識點(七十六)線程池進階

在之前的文章Java面試知識點(七十三)線程池 ,已經說了線程池的基本情況,包括線程池的運行原理,線程池的創建,任務的提交,獲取結果,線程池的關閉和配置,下面我們在深入的瞭解一下線程池並進行代碼編寫。

一、線程池的繼承架構

Java 裏面線程池的頂級接口是 Executor,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具。

真正的線程池接口是 ExecutorService。下面這張圖完整描述了線程池的類體系結構。
在這裏插入圖片描述

  • Executor 是一個頂層接口(類似一個標記接口),在它裏面只聲明瞭一個方法 execute (Runnable),返回值爲 void,參數爲 Runnable 類型,從字面意思可以理解,就是用來執行傳進去的任務的;

  • 然後 ExecutorService 接口繼承了 Executor 接口,並聲明瞭一些方法:submit、invokeAll、invokeAny 以及 shutDown 等;

  • 抽象類 AbstractExecutorService 實現了 ExecutorService 接口,基本實現了 ExecutorService 中聲明的所有方法;

  • ThreadPoolExecutor 繼承了類 AbstractExecutorService。

整理得:
在這裏插入圖片描述


二、線程池相關類

1.Executors 類

該類裏面提供了一些靜態工廠,生成一些常用的線程池。

  • newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行,使用的阻塞隊列是LinkedBlockingQueue;

  • newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程,使用的 LinkedBlockingQueue;

  • newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60 秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。 此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說 JVM)能夠創建的最大線程大小,newCachedThreadPool 將 corePoolSize 設置爲 0,將 maximumPoolSize 設置爲 Integer.MAX_VALUE,使用的 SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過 60 秒,就銷燬線程。

  • newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

  • newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。

  • 【源碼】

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

注意:聲明LinkedBlockingQueue的時候,可以指定大小,也可以不指定,不指定的時候,就是默認Integer.MAX_VALUE的大小,所以當阻塞隊列是linked並且不指定大小的時候,提交任務是否溢出是根據內存來確定的。而arrayblockingqueue必須指定大小


2.Future類

  • Future 表示異步計算的結果。
    它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。
    計算完成後只能使用 get 方法來獲取結果,如有必要,計算完成前可以阻塞此方法。

  • 取消則由 cancel 方法來執行。還提供了其他方法,以確定任務是正常完成還是被取消了。一旦計算完成,就不能再取消計算。

  • 如果爲了可取消性而使用 Future 但又不提供可用的結果,則可以聲明 Future<?> 形式類型、並返回 null 作爲底層任務的結果。
    在這裏插入圖片描述

  • Future 就是對於具體的 Runnable 或者 Callable 任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過 get 方法獲取執行結果,該方法會阻塞直到任務返回結果。

  • 也就是說 Future 提供了三種功能:

    • 判斷任務是否完成;

    • 能夠中斷任務;

    • 能夠獲取任務執行結果。

  • Future 類的方法

    • boolean cancel (boolean mayInterruptIfRunning) 試圖取消對此任務的執行。

    • V get () 如有必要,等待計算完成,然後獲取其結果。

    • V get (long timeout, TimeUnit unit) 如有必要,最多等待爲使計算完成所給定的時間之後,獲取其結果(如果結果可用)。

    • boolean isCancelled () 如果在任務正常完成前將其取消,則返回 true。

    • boolean isDone () 如果任務已完成,則返回 true。


三、代碼示例

1.無返回值的Runable示例

【實現runnable接口的線程類】

package test.threadpool;

public class Water implements Runnable {

    private int num;

    public Water(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        System.out.println("第" + num + "號選手入場");
        try {
            Thread.currentThread().sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第" + num + "號選手黯然離場");
    }
}

【生產環境】

package test.threadpool;

import java.util.concurrent.*;

public class Produce {
    public static void main(String[] args) {
        // 核心線程數
        int corePoolSize =5;
        // 線程池總大小
        int poolSize = 10;
        // 任務阻塞隊列
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue(5);
        // 創建線程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                poolSize,
                2000,
                TimeUnit.MICROSECONDS,
                queue);

		// 注意這裏的循環次數,當超過池和阻塞隊列的和之後,線程池會拒絕,使用默認的拒絕策略,即拋出異常
        for (int i=1; i<16; i++) {
            Water task = new Water(i);
            executor.execute(task);
            System.out.println("線程池中當前線程數:"+executor.getPoolSize()+
                    ",等待執行的任務數:"+executor.getQueue().size()+
                    ",已經完成人數數:"+executor.getCompletedTaskCount());
        }

        // 關閉線程池
        executor.shutdown();

    }

}

【運行結果(不唯一)】

線程池中線程數:1,等待執行的任務數:0,已經完成人數數:01號選手入場
線程池中線程數:2,等待執行的任務數:0,已經完成人數數:02號選手入場
線程池中線程數:3,等待執行的任務數:0,已經完成人數數:03號選手入場
線程池中線程數:4,等待執行的任務數:0,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:0,已經完成人數數:04號選手入場
線程池中線程數:5,等待執行的任務數:1,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:2,已經完成人數數:05號選手入場
線程池中線程數:5,等待執行的任務數:3,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:4,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:6,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:7,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:8,等待執行的任務數:5,已經完成人數數:011號選手入場
第12號選手入場
線程池中線程數:9,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:10,等待執行的任務數:5,已經完成人數數:013號選手入場
第14號選手入場
第15號選手入場
第3號選手黯然離場
第14號選手黯然離場
第15號選手黯然離場
第1號選手黯然離場
第12號選手黯然離場
第5號選手黯然離場
第2號選手黯然離場
第11號選手黯然離場
第13號選手黯然離場
第4號選手黯然離場
第10號選手入場
第9號選手入場
第8號選手入場
第7號選手入場
第6號選手入場
第9號選手黯然離場
第7號選手黯然離場
第10號選手黯然離場
第8號選手黯然離場
第6號選手黯然離場

從執行結果可以看出,當線程池中線程的數目大於 5 時,便將任務放入任務緩存隊列裏面,當任務緩存隊列滿了之後,便創建新的線程。
如果上面程序中,將 for 循環中改成執行 20 個任務,就會拋出任務拒絕異常了。

注意:聲明LinkedBlockingQueue的時候,可以指定大小,也可以不指定,不指定的時候,就是默認Integer.MAX_VALUE的大小,所以當阻塞隊列是linked並且不指定大小的時候,提交任務是否溢出是根據內存來確定的。而arrayblockingqueue必須指定大小


2.有返回值的Callable示例

callable也是實現多線程的一種方式,但是單獨用的情況很少,大部分情況是和線程池一起使用

Runnable 和 Callable 的區別

  • Runnable 執行方法是 run (),Callable 是 call ()
  • 實現 Runnable 接口的任務線程無返回值;實現 Callable 接口的任務線程能返回執行結果
  • call 方法可以拋出異常,run 方法若有異常只能在內部消化

【實現callable接口】

package test.threadpool;

import java.util.concurrent.Callable;

public class CallabelImpl implements Callable {
    private int num;

    public CallabelImpl(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
        int result = 0;
        for (int i=0; i<=num; i++) {
            result += i;
            System.out.println("這是:"+num+" 的計算進度:"+result);
        }
        System.out.println("<<<<<<<<計算結束>>>>>>>");
        return result;
    }
}

【生產環境】

package test.threadpool;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Demo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        Future<Integer> f1 = pool.submit(new CallabelImpl(5));
        Future<Integer> f2 = pool.submit(new CallabelImpl(10));

        try {
            Integer i1 = f1.get();
            Integer i2 = f2.get();

            System.out.println(i1+"----"+i2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {

        }
        pool.shutdown();
    }
}

【運行結果(線程池大小爲1,結果唯一)】

這是:5 的計算進度:0
這是:5 的計算進度:1
這是:5 的計算進度:3
這是:5 的計算進度:6
這是:5 的計算進度:10
這是:5 的計算進度:15
<<<<<<<<計算結束>>>>>>>
這是:10 的計算進度:0
這是:10 的計算進度:1
這是:10 的計算進度:3
這是:10 的計算進度:6
這是:10 的計算進度:10
這是:10 的計算進度:15
這是:10 的計算進度:21
這是:10 的計算進度:28
這是:10 的計算進度:36
這是:10 的計算進度:45
這是:10 的計算進度:55
<<<<<<<<計算結束>>>>>>>
15----55

但是如果,把線程池的大小設置成大於等於2

ExecutorService pool = Executors.newFixedThreadPool(2);

【結果(不唯一)】

這是:10 的計算進度:0
這是:5 的計算進度:0
這是:10 的計算進度:1
這是:10 的計算進度:3
這是:10 的計算進度:6
這是:10 的計算進度:10
這是:5 的計算進度:1
這是:10 的計算進度:15
這是:10 的計算進度:21
這是:10 的計算進度:28
這是:10 的計算進度:36
這是:10 的計算進度:45
這是:10 的計算進度:55
<<<<<<<<計算結束>>>>>>>
這是:5 的計算進度:3
這是:5 的計算進度:6
這是:5 的計算進度:10
這是:5 的計算進度:15
<<<<<<<<計算結束>>>>>>>
15----55

分析:當線程池只有一個的時候,提交兩個任務,一個在corepool中一個在阻塞隊列中,在阻塞隊列中的任務會等待池中的任務運行結束,在進入池中開始執行。

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