多線程(一) 線程池的原理和構造

一、線程池概述

線程經常用來同時處理一個程序的多個任務,但是在併發任務非常多並且處理時間短的情況下,使用線程就需要面臨一個問題,假設我們把線程創建的時間看做T1,線程執行任務的時間看做T2,線程銷燬的時間看做T3,學過小學數學的都知道,只有當T2的時間足夠大時,這個線程才能執行更多的任務,而不是把時間花費在創建和銷燬上。

然而實際開發中,很少能人爲控制T2的時間,因此,如何縮減T1和T3的時間,就是目前的解決思路,也就是線程池對線程資源開銷的解決思路。

線程池將一個或者多個線程多次反覆利用,同時需要對併發任務進行處理(也就是在任務增加的情況下,動態的創建新的線程),同時也需要考慮在程序空閒的情況下,對資源的釋放(也就是在沒有新的任務的情況下,銷燬已有的線程,釋放佔用的資源)。

 

二、線程池構造

通常我們通過Java提供的ThreadPoolExecutor類來配置一個線程池對象。

這個類重要的構造參數主要有以下幾個:

1)corePoolSize:核心線程數

該參數決定線程池能夠一直活躍的線程數量。每當一個任務交給線程池,只要當前核心線程數小於corePoolSize,那麼就會新建一個線程運行新來的任務。

2)BlockingQueue<Runnable> workQueue:等待隊列

隊列一般分爲有界隊列和無界隊列(無界隊列因爲會無限擴容的原因,一旦處理速度跟不上任務產生速度,任務堆積,就會導致內存膨脹溢出)。

核心線程數已經達到了設定大小時,新來的任務會交給等待隊列。

3)maximumPoolSize:最大線程數

等待隊列也滿了的時候,線程池會進入996加班模式,也就是會繼續new Thread來處理新來的任務(如果設置的maximumPoolSize==corePoolSize,那麼這個線程池不會創建新的線程。)

4)RejectedExecutionHandler handler:拒絕執行策略

當一個任務被拒絕執行時(比如線程池和等待隊列都爆滿的情況下),會調用handler的rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法(如果有的話)。

5)keepAliveTime:保持活躍時間

這個參數主要用來限制非核心線程的存活時間。 確保不會有線程長時間進入空閒狀態,佔用資源。

但是該參數對核心線程無效,核心線程一旦被創建,除非人爲關閉,否則一直存活。

6)TimeUnit timeUnit:keepAliveTime的單位

該參數是枚舉類型,用來指定keepAliveTime的時間單位(秒,毫秒……),可以賦值TimeUnit.SECONDS表示秒。

7)ThreadFactory threadFactory:線程工廠

顧名思義,該對象主要用來創建線程對象。(JDK有提供默認的線程創建模式。)

 

三、線程池的創建

的基礎之上,我們就可以開始構造適用於自己的線程池了。

爲了演示線程創建和拒絕策略的執行,其中ThreadFactory RejectedExecutionHandler的實現需要自己手寫一下。

在這裏我先寫好一個任務,繼承了Runable接口

//因爲是內部類所以加了static,如果不寫成內部類,就不需要寫
static class MyTask implements Runnable {
        private String taskName;

        public MyTask(String taskName) {
            this.taskName = taskName;
            System.out.println(getTaskName() + "is create");
        }

        @Override
        public void run() {
            System.out.println(getTaskName() + "is running");
            try {
                Thread.sleep(2500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getTaskName() + "is over");
        }

        public String getTaskName() {
            return "任務"+taskName;
        }

        @Override
        public String toString() {
            return "任務"+taskName;
        }
    }

接着實現一下線程池中ThreadFactory的創建

static class MyThreadFactory implements ThreadFactory {
        private int threadNo = 0;

        @Override
        public Thread newThread(Runnable r) {
            //線程池會調用該方法來創建線程對象
            ++threadNo;
            System.out.println("Thread" + threadNo + "is created");
            return new Thread(r,String.valueOf(threadNo));
        }
    }

接着準備一下拒絕執行策略

static class MyRejectedHandler implements RejectedExecutionHandler {

        //線程池在拒絕一個任務的請求之後,會調用該方法
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            
            MyTask mt = (MyTask) r;
            //獲取此時的隊列
            BlockingQueue<Runnable> workQueue = executor.getQueue();
            System.err.print(mt.getTaskName() + " is rejected,原因:");
            System.err.print("等待隊列當前大小:"+workQueue.size() + "已經達到最大值,");
            //獲取迭代器循環遍歷所有的任務,取出任務名並且打印
            Iterator<Runnable> iterator = workQueue.iterator();
            String message = "";
            while(iterator.hasNext()){
                message += iterator.next().toString()+" ";
            }
            System.err.println("等待隊列的任務是"+message);
        }
    }

現在萬事俱備,可以創建線程池了

public static void main(String[] args) {
        //核心線程數
        int corePoolSize = 2;

        //最大線程數
        int maximumPoolSize = 4;

        //保持生命時間:爲了節約資源,當一個線程空閒時間超過keepAliveTime時,該線程會被註銷(默認對核心線程無效)
        long keepAliveTime = 2;

        //keepAliveTime的時間單位,屬於枚舉類型
        TimeUnit timeUnit = TimeUnit.SECONDS;

        //等待隊列:構造函數表示隊列的大小,當所有的核心線程都在忙碌時,新來的任務會被存儲在此
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);

        //線程創建工廠:工廠設計模式,用來構建線程對象
        ThreadFactory threadFactory = new MyThreadFactory();

        //拒絕執行後處理器:當等待隊列已滿,新來的任務會被拒絕執行,此時會調用處理器的rejectedExecution方法來處理異常
        RejectedExecutionHandler handler = new MyRejectedHandler();

        //創建線程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, handler);

        //創建核心線程
        int coreNo = threadPoolExecutor.prestartAllCoreThreads();
        System.out.println(coreNo+"個核心線程被創建");

        //循環創建任務
        for (int i=1;i<=10;i++){
            threadPoolExecutor.execute(new MyTask(String.valueOf(i)));
        }

        //程序執行之後不會自己停止,因爲線程池還掛着呢。需要手動釋放線程才能結束程序。
        


    }

最後看一下運行結果吧。

可以看到,一開始手動創建了所有的核心線程(空閒狀態),之後開始循環創建任務。

接着任務1和任務2因爲此時核心線程空閒,直接運行,任務3和任務4則在等待隊列中等待。

之後又創建了任務5,此時核心線程和等待隊列已經滿了,但是線程數還沒有達到最大,因此創建了新的線程3來執行這個新任務。

任務6和任務5的待遇相同。因爲線程的創建需要時間,因此任務的運行和創建之間等待的時間較長。

任務7,8,9,10因爲此時的線程數已經達到最大,等待隊列也爆滿,因此,全部都被拒絕執行並且執行了拒絕策略。

接着,任務1和任務2執行完畢,等待隊列的任務3和任務4繼續執行。

最後所有的任務都執行完畢,程序……還沒停。

需要手動銷燬線程池之後,程序纔會正常關閉

 

 

四、線程池的關閉

一般情況下,線程池都是爲了解決併發項目的任務而開啓的,所以並不需要關閉線程池。但是如果想要關閉線程池的話,由於核心線程並不會自己銷燬,因此需要手動結束線程,線程池想要自動銷燬的話,需要滿足兩個條件。

  1. 線程池引用不可達。
  2. 線程池的線程數爲0。

推薦通過調用線程池提供的方法來關閉線程。

主要有以下幾個方法:

  • shutdown():該方法會準備關閉線程池,執行此方法,會等待所有任務結束之後,再關閉線程池。
  • shutdownNow():該方法會立刻關閉線程池,執行此方法,正在執行的任務會被強制中斷。
  • getActiveCount():該方法會返回當前正在工作的線程數,當所有的線程都處於空閒狀態時,返回值爲0。

當線程池被關閉之後,線程池將無法再執行新的任務,否則會拋出異常!(準確來說,會執行拒絕執行策略代碼)。

 

五、線程關閉機制

雖然在之前提到,使用shutdownNow()可以立即關閉線程池,但是其實質上並不是如此。

追蹤源碼我們可以發現,線程池的shutdownNow()方法,本質上還是調用了線程本身的關閉方法interrupt()。所以,shutdownNow()方法也不能保證立即關閉線程池。

void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    //shutdownNow()的底層實現是調用了線程對象的interrupt方法
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }

因爲線程的關閉方法interrupt() 其本質上並沒有關閉線程,能夠直接關閉線程的stop()等方法都因爲線程安全問題而廢棄。interrupt()方法僅僅只是更改了線程對象的一個標誌位,告訴這個線程,應該要關閉了,但是實際上是否關閉,取決於線程本身。因此,即便使用showdownNow()方法之後,線程池中的線程關閉還是不關閉,也要取決於線程自身。。。

這裏根據應用場景有多個不同解決方案,如果任務是經常做循環或者遞歸操作,那麼可以用這樣的方式:

        @Override
        public void run() {
            for (int i=0;i<1000000;i++){
                //在方法體中添加校驗關閉標誌位,判斷線程是否應該關閉
                if(Thread.interrupted()){
                    System.out.println("退出線程"+i);
                    break;
                }else{
                    i++;
                }
            }
        }

這個程序在使用線程池運行,並且使用shutdownNow()關閉後,結果如下

可以發現,該任務的循環並沒有結束,線程就被關閉了。

如果去掉校驗,改成這樣:

        @Override
        public void run() {
            int i = 0;
            for (;i<1000000;i++){
                //在方法體中去掉標誌位
                    i++;
            }
             System.out.println("退出線程"+i);
        }

運行結果:

即便使用了shutdownNow(),線程依舊做完了循環才關閉,這樣看來在多次循環中如果不對標誌位認爲判斷,shutdownNow()和shutdown()幾乎沒什麼區別了。

 

 

 

參考原文地址:https://www.jianshu.com/p/f030aa5d7a28

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