Java基礎(十四)——線程池

池化技術
池化技術有很多實現,例如線程池,數據庫連接池,Http連接池等都是池化技術的具體應用,

主要思想就是減少每次獲取資源的消耗,提高對資源的利用率

線程池簡介
線程池提供了一套限制和管理資源的功能,還維護了一些基本的數據統計,例如已完成任務的數量

使用線程池的好處:

降低資源消耗
提升響應速度
提高線程的可管理性
Java對線程池的管理主要是由Executor來負責的

Executor
Executor框架是在Java5之後引進的,使用Executor框架比直接使用Thread的start的方法來啓用線程要好得多,除了易於管理,效率更高的優點外,還有關鍵的一點:有助於避免this逃逸問題

this逃逸:

​ 當一個線程在調用對象的構造方法時,另一個線程直接使用了這個還沒構造好的對象引發的一系列令人迷惑的問題

Executor還提供了線程工廠,隊列以及拒絕策略等

常見的實現類:ThreadPoolExecutor 和 ScheduledThreadPoolExecutor類,他們之間的繼承關係如下圖所示:

繼承關係

ThreadPoolExecutor介紹
1.構造方法:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
其他的構造器都是直接調用的此構造器,進行了些默認配置

參數說明:

corePoolSize:最小可以同時運行的線程適量(核心線程的個數)
maximumPoolSize:最大線程數(核心線程+非核心線程)
keepAliveTime:當線程池的數量大於corePoolSize的時候,如果這時候沒有新的任務提交,在線程池外的線程不會立即銷燬,而是會等待keepAliveTime,超過時間纔會被銷燬(非核心線程的存在時間)
unit:keepAliveTime的時間單位
workQueue:當新任務來了之後會先判斷線程池是否滿,如果滿了就將任務暫存到workQueue中
threadFactory:創建新線程的時候會調用到線程工廠
handler:飽和策略,後面會介紹
流程圖

流程圖

將要執行的任務傳進線程池中去,線程池會先用任務隊列來接受任務(如果核心線程數都被佔用了的話),非核心線程的存在時間爲定義的keepAliveTime。

ThreadPoolExecutor的飽和策略:

如果當前運行線程數達到最大並且任務隊列已經排滿了任務時候,有以下幾種處理策略:

ThreadPoolExecutor.AbortPolicy:直接拋出RejectedExecutionException異常
ThreadPoolExecutor.CallerRunsPolicy:讓傳遞該任務的線程調用execute()方法來完成這個任務,但是會降低系統運行速度,如果能接受運行速度下降並且不能有任何消息丟失的話,使用這個策略
ThreadPoolExecutor.DiscardPolicy:不拋出異常,直接丟棄
ThreadPoolExecutor.DiscardOldestPolicy:丟棄最早的未處理的任務
Spring通過ThreadPoolTaskExecutor或者我們自己創建ThreadPoolExecutor的時候默認使用的是ThreadPoolExecutor.AbortPolicy策略

推薦使用線程池代替線程的顯示創建

​ 阿里開發規範中,明確指出線程資源必須由線程池提供,不能在應用中顯示的創建線程,並且使用ThreadPoolExecutor的構造函數來取代Executors的創建方式,這樣更能明確線程的創建規則,避免資源耗盡的風險

ThreadPoolExecutor小Demo
Runnable的Demo

線程池:

public class ThreadPoolExecutorDemo {
//線程池基本參數:線程池的最值,任務隊列的長度,持續時間
private static final Integer CORE_POOL_SIZE = 5;
private static final Integer MAX_POOL_SIZE = 10;
private static final Integer QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;

public static void main(String[] args) {
    //線程池創建
    ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAX_POOL_SIZE, KEEP_ALIVE_TIME,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY),
            new ThreadPoolExecutor.CallerRunsPolicy());

    //任務創建
    for (int i = 0; i < 10; i++) {
        MyRunnable runnable = new MyRunnable("" + i);
        executor.execute(runnable);
    }

    //終止線程池
    executor.shutdown();

    while (!executor.isTerminated()){

    }
    System.out.println("Finish All Task");
}

}
executor.isTerminated函數:

​ 當調用shudown或者shutdownNow方法後,所有任務提交完成後返回true,如果有任務沒完成則返回false。while循環目的:防止有線程還在執行,而程序提前結束。

任務:

public class MyRunnable implements Runnable {

private String command;

public MyRunnable(String command) {
    this.command = command;
}

@Override
public void run() {
    System.out.println(Thread.currentThread().getName() + " Start Time : " + new Date());
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " End Time : " + new Date());
}

}
輸出結果:

pool-1-thread-3 Start Time : Fri Mar 20 14:59:07 CST 2020
pool-1-thread-1 Start Time : Fri Mar 20 14:59:07 CST 2020
pool-1-thread-5 Start Time : Fri Mar 20 14:59:07 CST 2020
pool-1-thread-4 Start Time : Fri Mar 20 14:59:07 CST 2020
pool-1-thread-2 Start Time : Fri Mar 20 14:59:07 CST 2020
pool-1-thread-3 End Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-1 End Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-3 Start Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-2 End Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-1 Start Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-5 End Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-4 End Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-5 Start Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-4 Start Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-2 Start Time : Fri Mar 20 14:59:12 CST 2020
pool-1-thread-1 End Time : Fri Mar 20 14:59:17 CST 2020
pool-1-thread-4 End Time : Fri Mar 20 14:59:17 CST 2020
pool-1-thread-3 End Time : Fri Mar 20 14:59:17 CST 2020
pool-1-thread-2 End Time : Fri Mar 20 14:59:17 CST 2020
pool-1-thread-5 End Time : Fri Mar 20 14:59:17 CST 2020
Finish All Task
簡單分析:

execute()方法執行過程

image-20200320150235100

常見的比較
1.Runnable和Callable

Runnable自1.0起就一直存在,而Callable在1.5之後才進行了補充,目的就是爲了解決Runnable無法拋出異常和無返回值的短板,如果不需要拋出異常和返回值,建議使用Runnable,迫不得已再去使用Callable

工具類Executors實現了Runnable和Callable之間的轉換

2.execute()和submit()

execute方法用來提交不需要返回值的任務,即Runnable實現類,無法判斷任務是否執行成功

submit方法用來提交需要返回值的任務,即Callable的實現類,返回一個Future對象,可以通過Future對象來判斷是否執行成功,並且可以使用get方法來獲取返回值【get方法是阻塞的】,可以使用get的重載方法get(long timeout,TimeUtil util)來阻塞一段時間後強制獲取,有可能出現沒有執行完的情況

3.shutdown()和shutdownNow()

shutdown:關閉線程池,線程池狀態變爲SHUTDOWN,不再接受任何任務,但是仍舊會執行隊列中的任務

shutdownNow:關閉線程池,線程池狀態變爲STOP,終止當前任務和排隊的任務,返回排隊任務List

4.isShutdown()和isTerminated()

isShutdown:當調用shutdown方法後返回爲true

isTerminated:調用shutdown後,所有任務提交完成即返回true,或者調用shutdownNow成功後返回true

ThreadPoolExecutor小Demo
Callable的demo

線程池:

public class ThreadPoolExecutorDemo {
//線程池基本參數:線程池的最值,任務隊列的長度,持續時間
private static final Integer CORE_POOL_SIZE = 5;
private static final Integer MAX_POOL_SIZE = 10;
private static final Integer QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //線程池創建
    ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAX_POOL_SIZE, KEEP_ALIVE_TIME,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY),
            new ThreadPoolExecutor.CallerRunsPolicy());

    ArrayList<Future<String>> futureList = new ArrayList<>();
    MyCallable callable = new MyCallable();
    for (int i = 0; i < 10; i++) {
        Future<String> future = executor.submit(callable);
        futureList.add(future);
    }

    //便利Future
    for (Future<String> i : futureList) {
        System.out.println(new Date() + "   " + i.get());
    }

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

    while (!executor.isTerminated()){}

    System.out.println("Finish All Task");
}

}
任務:

public class MyCallable implements Callable {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(1);
return Thread.currentThread().getName();
}
}
輸出:

Fri Mar 20 15:28:19 CST 2020 pool-1-thread-1
Fri Mar 20 15:28:20 CST 2020 pool-1-thread-2
Fri Mar 20 15:28:20 CST 2020 pool-1-thread-3
Fri Mar 20 15:28:20 CST 2020 pool-1-thread-4
Fri Mar 20 15:28:20 CST 2020 pool-1-thread-5
Fri Mar 20 15:28:20 CST 2020 pool-1-thread-2
Fri Mar 20 15:28:21 CST 2020 pool-1-thread-4
Fri Mar 20 15:28:21 CST 2020 pool-1-thread-1
Fri Mar 20 15:28:21 CST 2020 pool-1-thread-5
Fri Mar 20 15:28:21 CST 2020 pool-1-thread-3
Finish All Task
常見的連接池比較
與其說是幾個類,不如說是指定了部分參數的ThreadPoolExecutor

1.FixedThreadPool
通過executors工具類來創建出來的線程池,可重用固定線程數

調用public static ExecutorService newFixedThreadPool(int nThreads)或者

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)來實現創建

參數說明:

​ nThreads:設置corePoolSize和maxinumPoolSize的值

所以FixedThreadPool的非核心線程爲零。

爲什麼不推薦使用FixedThreadPool:

1.線程池中無非核心線程

2.FixedThreadPool的任務隊列問LinkedBlockingQueue,是用鏈表實現的無界隊列,有可能造成OOM

2.SingleThreadExecutor
SingleThreadExecutor也是使用Executors工具類創建的,只有單個核心線程,隊列使用的也是LinkedBlockingQueue

不推薦原因:

1.只有一個核心線程,利用率低

2.和FixedThreadPool一樣,可能會造成OOM

3.CachedThreadPool
CachedThreadPool的實現:

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue(),
threadFactory);
}
核心線程被設置成爲0,非核心線程被設置成無窮大,如果當主線程的提交任務的速度大於CachedThreadPool的處理速度,那麼會一直創建線程

不推薦使用CachedThreadPool的理由:如果處理器速度不行,就會創建大量線程,從而導致OOM

ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor主要作用是在給定的時間後再執行任務,或者是定時執行任務,但是在實際開發中一般用的比較少,因爲SpringBoot整合了自己的Schedule Task

簡介:

使用了內部類DelayedWorkQueue,會對隊列中的任務進行按時間順序的排序,執行所需時間短的在前面執行,若執行時間相同,則按照任務加入隊列的順序依次執行,在JDK1.5之後纔出,代替了Timer

執行步驟:

當嗲用ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWirhFixedDelay方法時,會向ScheduledThreadPoolExecutor實例化對象中的DelayQueue中傳入一個實現了RunnableScheduledFuture接口的ScheduledFutureTask,線程池再從DelayQueue中獲取ScheduledFutureTask,再執行任務

ScheduledThreadPoolExecutor與ThreadPoolExecutor的對比:

使用了DelayQueue作爲隊列任務
獲取任務的方法不同
執行週期任務後,多了額外的處理
流程圖

流程簡介:

線程 1 從 DelayQueue 中獲取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任務是指 ScheduledFutureTask 的 time 大於等於當前系統的時間;
線程 1 執行這個 ScheduledFutureTask;
線程 1 修改 ScheduledFutureTask 的 time 變量爲下次將要被執行的時間;
線程 1 把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。
線程池大小的確定
大部分程序員在設定線程池的大小的時候都是按照自己的意願隨心而定

危害:

如果設置太少,會導致大量的任務堆積在工作隊列中,如果隊列滿了,根據線程池的飽和策略來定,有可能出現任務丟失,或者出現OOM,這種情況就是內存壓力過大,CPU沒有得到充分利用

如果設置太多,會導致大量線程都在爭取CPU資源,會有大量的上下文切換,增加線程整體的執行時間,效率降低

解決公式:

CPU密集型任務(N+1):主要壓力在CPU這邊,線程數設置成N(CPU核心數)+1,多的一個線程是爲了及時填補上意外情況導致的任務暫停
IO密集型任務(2N):這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用,具體的計算方法是 2N。

歡迎關注我們github同步項目
Java-Knowledge-Architecture

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