從Java線程到線程池

線程模型

線程模型分爲兩類,用戶級線程(ULT)和內核級線程(KLT)

  • 用戶級線程(ULT):user level threads,系統內核對ULT無感知,線程的創建和調度都由用戶級APP進程管理;即APP自行管理的線程,就是用戶級線程

  • 內核級線程(KLT):kernel level threads,線程的創建,調度和切換上下文等,都由操作系統內核管理

    如圖所示,ULT模型中,每個進程創建的線程,均由自身進程維護管理,而KLT模型中,進程創建的線程,由系統內核進行維護管理。

​ 在執行的區別之上,ULT和KLT最大的區別就是:ULT線程的調度不需要內核直接參與,控制簡單,創建和銷燬線程、線程切換代價等線程管理的開銷比內核線程少得多。但是其資源調度按照進程進行,不能利用系統的多核處理,多個處理器下,同一個進程中的線程只能在同一個處理器下分時複用;KLT由內核進行調度,當有多個處理器時,一個進程的多個線程可以同時執行,但是KLT線程的創建和切換等開銷很大。

​ Java線程是依賴於系統內核,通過JVM調用系統庫創建內核線程。Java的Thread和內核線程呈 1:1映射關係。by the way,這也是爲什麼我們需要線程池進行線程池化管理的原因。

線程池

基礎概念

​ 線程是程序運行的載體,說白了就是擁有cpu多長時間的執行權。而由於java線程是內核級線程帶來的昂貴開銷成本,在平時運用場景中我們也經常使用線程池對線程進行池化管理。線程池能爲我們帶來3大好處:

  1. 複用已創建的線程,降低創建銷燬線程等帶來的資源消耗
  2. 提高響應速度。線程早已創建好,不需要在等待線程創建就可以直接使用
  3. 可管理線程。由於線程是稀有資源,不能無限創建,因此用線程池能對線程進行統一規劃,監控和分配

在線程池中,有幾項概念最爲重要:核心線程數,最大線程數,等待隊列,飽和策略。它們的執行過程如下圖。

在這裏插入圖片描述

爲加強記憶,可以用工廠幹活的方式理解。把核心線程數看作是正式工,當來了一堆任務的時候,正式工陸續開始幹活,當任務增多的時候,正式工們手頭上的任務還沒幹完,於是先放阻塞隊列等待待會兒處理;後來任務量暴漲,阻塞隊列也滿了,於是就請一些臨時工來幹活,於是,正式工數+臨時工數 = 最大線程數;再後來線程數持續增長,最大線程數也滿了,只能做一些飽和策略限制任務的提交了。

線程池的使用

​ 我們可以通過new ThreadPoolExecutor的方式創建線程池。

//構造方法
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
  
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory)
  
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)
  
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

其中的參數需要說明下:

corePoolSize:核心線程數。線程池在最初階段,池中線程數爲0,當接收到一個任務後,就創建一個線程。即使之前的線程執行完畢空閒,新任務提交過來,依然會創建線程。直到創建線程到核心線程數。如果調用了prestartAllCoreThreads方法,那麼線程池會預先創建好線程。

BlockingQueue: ⽤於保存等待執⾏的任務的阻塞隊列,可參考JUC的7種阻塞隊列,這裏不再贅述

maximumPoolSize:線程池允許創建的最⼤線程數。如果隊列滿了,並且已創建的線程數⼩於最⼤線程數,則線程池會再創建新的線程執⾏任務

TimeUnit:線程活動保持時間的單位

keepAliveTime:線程活動保持時間。當線程池中創建的線程數量超過核心線程數,且有線程空閒超過該時間,空閒線程將會被回收至核心線程數大小。當設置爲0,即只要線程空閒就立即回收

ThreadFactory:⽤於設置創建線程的⼯⼚,可以通過線程⼯⼚給每個創建出來的線程設置更有意義的名字。附錄附上線程工廠的用法例子。

RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取⼀種策略處理提交的新任務。這個策略默認情況下是AbortPolicy。在JDK1.5中Java線程池框架提供了以下4種策略。

  • AbortPolicy:直接拋出異常。

  • CallerRunsPolicy:當任務添加到線程池中被拒絕時,會在線程池當前正在運行的Thread線程池中處理被拒絕的任務。

  • DiscardOldestPolicy:線程池會放棄等待隊列中最舊的未處理任務,然後將被拒絕的任務添加到等待隊列中。

  • DiscardPolicy:不處理,丟棄掉。

當然也可以自定義,在附錄舉了一個demo可參考。

舉上一個較爲完整的線程池創建示例

BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);

ThreadPoolExecutor threadPoolExecutor =
  new ThreadPoolExecutor(5, 10, 1000, TimeUnit.MILLISECONDS, workQueue, new ThreadPoolExecutor.AbortPolicy());

線程池的工作狀態

線程池共有5種生命狀態:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED

在這裏插入圖片描述

  • RUNNING:接受新任務並處理排隊的任務

  • SHUTDOWN:不接受新任務,但處理排隊的任務

  • STOP:不接受新任務,不處理排隊的任務,中斷正在進行的任務

  • TIDYING:所有任務都已終止,workerCount爲零,將運行terminate()鉤子方法

  • TERMINATED:線程池終止

線程池提交任務的方式有兩種,分別爲execute()和 submit()⽅法。

execute()⽅法⽤於提交不需要返回值的任務,⽆法判斷任務是否被線程池執⾏成功。

submit()⽅法⽤於提交需要返回值的任務。線程池會返回⼀個future類型的對象,可通過future對象判斷線程是否執行成功,獲取返回值等。

線程池關閉的方式也有兩種,shutdown()和shutdownNow()⽅法可關閉線程池。它們的原理是遍歷線程池中的⼯作線程,然後逐個調⽤線程的interrupt⽅法來中斷線程,所以⽆法響應中斷的任務可能永遠⽆法終⽌。

區別在於,shutdownNow 更改狀態成STOP,然後嘗試停⽌所有的正在執⾏或暫停任務的線程,並返回等待執⾏任務的列表,⽽shutdown更改狀態成SHUTDOWN狀態,然後中斷所有沒有正在執⾏任務的線程,正在執行的線程會繼續執行完畢。

1.線程池設置多少合適:

有大佬給出了詳細計算模式
https://dayarch.top/p/how-many-threads-should-be-created.html

2. 線程工廠的demo

/**
 * 實現線程工廠的接口,然後實現自定義內容。重寫newThread方法
 */
public class MyThreadFactory implements ThreadFactory {

    private static int COUNTER = 0;

    private static String THREAD_PREFIX = "myThread";

    @Override
    public Thread newThread(Runnable r) {
        int i = COUNTER++;
        return new Thread(r, THREAD_PREFIX + i);
    }

  	//使用
    public static void main(String[] args) {
        MyThreadFactory myThreadFactory = new MyThreadFactory();
        Thread thread = myThreadFactory.newThread(new Runnable() {
            @Override
            public void run() {

            }
        });
        thread.start();
    }
}

2. 自定義飽和策略

//需要實現RejectedExecutionHandler,重寫rejectedExecution方法
class MyRejected implements RejectedExecutionHandler {
    public MyRejected() {
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("現線程池中的情況爲:" + executor.getActiveCount());
        System.out.println("當前被拒絕任務爲:" + r.toString());
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章