五分鐘帶你搞懂線程池的線程複用原理

​前言

博客有一個多月沒更新了,主要是因爲剛換了工作,需要適應一下新環境,另外新公司正好趕上了幾個比較忙的項目,每天晚上到家就比較晚了,實在是分身乏術,不過該更新還是要更新滴,寫博客貴在堅持,今天就來講一下線程池的複用原理吧,希望能對你有所幫助!

線程池的作用及創建方式

提起線程,相信大家並不陌生,它可以幫助我們異步處理任務,提高CPU的利用率。在平時的開發中我們通常會利用線程池來創建和使用線程,這樣我們可以對線程進行重複利用,避免頻繁創建線程帶來的內存開銷,它會自動幫助我們對線程資源進行調度,簡單,方便,快捷。

在日常的開發中,我們可以使用JDK自帶的線程池工具類Executor來創建和使用線程池,創建方式及其優缺點如下:

//根據線程的使用情況自行創建線程,按需分配,優點:自動調整線程池大小,缺點:無限制創建,容易OOM
Executors.newCachedThreadPool();
​
//創建指定線程數量的線程池 優點:可以創建指定數量 缺點:不夠靈活
Executors.newFixedThreadPool(5);
​
//同newCachedThreadPool,只不過可以指定核心線程數,不常用
Executors.newScheduledThreadPool(5);
​
//創建只存在一個線程的線程池 優點:可以保證任務順序指定 缺點:cpu利用率較低
Executors.newSingleThreadExecutor();

由於JDK自帶的工具類有較多的侷限性,所以我們通常會自定義線程池的參數來手動創建線程,阿里的編碼規約中也強制要求這樣做,使用方法如下:

public ThreadPoolExecutor(int corePoolSize, 
                          int maximumPoolSize, 
                          long keepAliveTime, 
                          TimeUnit unit, 
                          BlockingQueue<Runnable> workQueue, 
                          RejectedExecutionHandler handler)
                          
/*corePoolSize : 線程的核心線程數
​
  maximumPoolSize:線程允許的最大線程數
  
  keepAliveTime:當前線程池線程總數大於核心線程數時,終止多餘的空閒線程的時間
  
  Unit:keepAliveTime:參數的時間單位
  
  workQueue:任務緩存隊列,如果線程池達到了核心線程數,並且其他線程都處於活動狀態的時候,則將新任務放入此隊列
  
  threadFactory:定製線程的創建過程
  
  Handler:拒絕策略,當workQueue隊列滿時,採取的措施*/

通過new ThreadPoolExecutor來創建線程池的方式明顯更加靈活,可定製性更強,JDK提供的Executor工具類本質也是new 了一個ThreadPoolExecutor,感興趣的朋友可以通過源碼自行查看。

線程池的使用方法

線程池的使用方法也非常簡單,在這裏舉個簡單的例子,如下:

ExecutorService fixExecutor = Executors.newFixedThreadPool(5);
//執行線程池中的任務
fixExecutor.execute(() -> {
    System.out.println(Thread.currentThread().getName() + ":線程運行!");
});
//運行完任務後停止運行
fixExecutor.shutdown();

雖然線程池使用起來容易,但在看的你可能對線程池中線程的複用原理並不瞭解,下面我們手寫一個簡易線程池來幫助理解。

手寫線程池

對於線程池的複用原理,可以簡單的用一句話概括:創建指定數量的線程並開啓,判斷當前是否有任務執行,如果有則執行任務。再通俗易懂一些:創建指定數量的線程並運行,重寫run方法,循環從任務隊列中取Runnable對象,執行Runnable對象的run方法。

圖示:

接下來開始手寫線程池吧,注意是簡易線程池,跟JDK自帶的線程池無法相提並論,在這裏我省略了判斷當前線程數有沒有大於核心線程數的步驟,簡化成直接從隊列中取任務,對於理解原理來說已然足矣,代碼如下:

public class MyExecutorService {
    /**
     * 一直保持運行的線程
     */
    private List<WorkThread> workThreads;
​
    /*
     * 任務隊列容器
     */
    private BlockingDeque<Runnable> taskRunables;
     /*
     * 線程池當前是否停止
     */
    private volatile boolean isWorking = true;
​
    public MyExecutorService(int workThreads, int taskRunables) {
        this.workThreads = new ArrayList<>();
        this.taskRunables = new LinkedBlockingDeque<>(taskRunables);
        //直接運行核心線程
        for (int i = 0; i < workThreads; i++) {
            WorkThread workThread = new WorkThread();
            workThread.start();
            this.workThreads.add(workThread);
        }
    }
    
    /**
     * WorkThread累,線程池的任務類,類比JDK的worker
     */
    class WorkThread extends Thread {
        @Override
        public void run() {
            while (isWorking || taskRunables.size() != 0) {
                //獲取任務
                Runnable task = taskRunables.poll();
                if (task != null) {
                    task.run();
                }
            }
        }
    }
    //執行execute,jdk中會存在各種判斷,這裏省略了
    public void execute(Runnable runnable) {
        //把任務加入隊列
        taskRunables.offer(runnable);
    }
​
    //停止線程池
    public void shutdown() {
        this.isWorking = false;
    }
}

代碼有具體註釋,耐心看一看非常容易理解,下面測試一下:

//測試自定義的線程池
public static void main(String[] args) {
    MyExecutorService myExecutorService = new MyExecutorService(3, 6);
    //運行8次
    for (int i = 0; i < 8; i++) {
        myExecutorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "task begin");
        });
    }
    myExecutorService.shutdown();
}

執行結果如下:

測試成功,手寫線程池到此結束!

總結

通過以上分析並手寫線程池,我們應該已經基本理解了線程池的複用機制原理,實際上JDK的實現機制遠比我們手寫的要複雜的多,主要有以下兩點,可以讓我們進一步加深理解:

1.當有新任務來的時候,首先判斷當前的線程數有沒有超過核心線程數,如果沒超過則直接新建一個線程來執行新的任務,如果超過了則判斷緩存隊列有沒有滿,沒滿則將新任務放進緩存隊列中,如果隊列已滿並且線程池中的線程數已經達到了指定的最大線程數,那就根據相應的策略拒絕任務,默認爲拋異常。

2.當緩存隊列中的任務都執行完畢後,線程池中的線程數如果大於核心線程數並且已經超過了指定的存活時間(存活時間通過隊列的poll方法傳入,如果指定時間內沒有獲取到任務,則break退出,線程運行結束),就銷燬多出來的線程,直到線程池中的線程數等於核心線程數。此時剩餘的線程會一直處於阻塞狀態,等待新的任務到來。

好了,以上就是對線程池複用機制的具體分析,這點在面試中也屬於高頻問題,還是需要我們掌握一下的,學習一門技術,做到知其然,知其所以然,才能做到融會貫通,發揮出它真正的作用!

ps:代碼生成器近期會更新噠,別催啦。。

喜歡的朋友可以關注公衆號 螺旋編程極客 獲取最新內容更新哦!

 


 

 

 

 

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