線程的創建及線程池

 

目錄

線程的創建

繼承Thread類

實現Runnable接口

實現Callable接口

線程池

執行流程

線程池排隊策略

拒絕策略

Executors的四種線程池

CompletionService

小結


前面講了線程的六種狀態及常見方法的比較,此節主要學習小結下線程的創建和線程池相關的一些知識。

線程的創建

1.繼承Thread類,重寫run方法(其實Thread類本身也實現了Runnable接口);
2.實現Runnable接口,重寫run方法;
3.實現Callable接口,重寫call方法(有返回值);
4.使用線程池(有返回值);
     線程是進程的一個執行單元,本質都是在實現一個線程任務。線程是多線程的形式上實現方式主要有兩種:一種是繼承Thread類,一種是實現Runnable接口。以上是比較常用的四種創建線程的方式,都是對其的一個封裝,下面看一下其具體實現。

繼承Thread類

   通過JDK提供的Thread類,繼承Thread類,重寫Thread類的run方法即可。步驟:
   (1) 繼承thread類,實現run() 方法,具體要完成的task;  
   (2) 啓動線程,new Thread子類().start();
    這裏創建一個新的線程,都要新建一個Thread子類的對象,創建線程實際調用的是父類Thread空參的構造器,具體實現如下:

public class ExtentThreadTest extends Thread {

    private static Logger log = LoggerFactory.getLogger(ExtentThreadTest.class);

    public ExtentThreadTest(String threadName){
        this.setName(threadName);
    }

    @Override
    public void run() {
        //TODO  實現任務task
        log.info("線程run:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //啓動線程
            new ExtentThreadTest("MyThreadTest").start();
        }
    }
}

實現Runnable接口

    實現Runnable接口重寫run方法,這是一種用的很多的方式。其實Runnable就是一個線程任務,線程任務和線程的控制分離,這也就是上面所說的解耦。我們要實現一個線程,可以藉助Thread類,Thread類要執行的任務就可以由實現了Runnable接口的類來處理。具體步驟如下:

(1) 定一個線程任務類來實現Runnable接口;
(2) 實現run()方法,方法體中的代碼就是所執行的task;
(3) 創建線程控制類thread類,將任務作爲Thread類的構造方法傳入;
(4) 啓動線程;

Runnable接口代碼:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

實現實例:

public class RunnableThreadTest implements Runnable {

    private static Logger log = LoggerFactory.getLogger(RunnableThreadTest.class);

    //實現run方法,具體的任務實現
    @Override
    public void run() {
        log.info("Runnable thread test");
    }

    public static void main(String[] args) {

        //實例化線程任務類
        RunnableThreadTest task1 = new RunnableThreadTest();
        for (int i = 0; i < 5; i++) {
            //創建線程對象,並將任務提交給線程執行;
            new Thread(task1).start();
        }

        //函數式接口可用lamba表達式來實現
        Runnable task2 = () -> {
            log.info("lamba 方式實現 Runnable 任務線程");
        };
        for (int i = 0; i < 5; i++) {
            new Thread(task2).start();
        }
    }
}

ps:內部類的實現

不是新的方式,只是一種新的寫法。在有些場景只需要異步處理一次就可以採用此種寫法,避免了上面定義線程任務實現類。

public class AnonymousThreadTest {

    private static Logger log = LoggerFactory.getLogger(AnonymousThreadTest.class);

    public static void main(String[] args) {
        //基於Thread子類的實現
        new Thread() {
            @Override
            public void run() {
                log.info("AnonymousThreadTest 基於子類thread實現");
            }
        }.start();

        //基於接口的實現
        new Thread(() -> {
            log.info("基於接口類     Runnable方法的實現");
        }).start();
    }
}

實現Callable接口

    前面的兩種方式實現接口Runnable和繼承Thread類我們發現都沒有返回值,很多時候我們是需要得到任務執行後的一個反饋的,所以需要其中執行得到異常和返回值,這裏Callable接口就爲我們提供了這樣的便利。具體步驟:

   (1) 創建一個類實現Callable接口,實現call方法,可提供返回值;
   (2) 創建一個FutureTask,指定Callable對象,做爲線程任務;
   (3) 創建線程,指定線程任務。
   (4) 啓動線程;
Callable接口類,也是一個函數式接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

PS: Callable中可以通過範型參數來指定線程的返回值類型。通過FutureTask的get方法拿到線程的返回值。
實現實例:

public class CallableThreadTest {

    private static Logger log = LoggerFactory.getLogger(CallableThreadTest.class);

    public static void main(String[] args) {
        //第一步:創建線程任務
        Callable<Integer> taskCall = () -> {
            return 1;
        };

        //第二步:創建一個FutureTask,指定Callable對象作爲線程任務;
        FutureTask<Integer> futureTask = new FutureTask<>(taskCall);

        //第三步:創建線程,指定線程任務;
        Thread callThread = new Thread(futureTask);

        //第四步:啓動線程
        callThread.start();

        //得到線程執行的結果及響應的異常信息
        try {
            Integer result = futureTask.get();//這裏get是阻塞的等待;
            log.info("thread result:{}", result);
        } catch (Exception e) {
            log.error("Exception:", e);
        }
    }
}

線程池

      其實工作中用的最多的就是線程池,例如單個任務處理時間短,需要處理的任務數量大我們就可以採用線程池的方式去處理。
那爲什麼要使用線程池呢?
     如果當請求到達的時候就創建線程,有時候線程的創建和開銷可能比處理業務請求的時間和資源還要多,如果創建線程過多,可能會因爲系統過度消耗內存線程的過度切換使系統資源不足。爲了防止資源不足,我們必須採用“池化”技術來管理線程的創建和銷燬,合理利用有限的系統資源,使之能高效穩定的運行。
從上面我們可以知道單一或者循環創建線程存在以下弊端:
(1)不管是繼承子類thread或者是接口Runnable和Callbale創建線程,每次通過new Thread()創建對象性能不高;
(2)單一或者循環創建線程缺乏統一管理,頻繁的創建和銷燬無限定的線程,線程的切換通信競爭可能導致系統性能下降,資源的浪費;
(3)單一的線程創建不夠靈活,如定時執行、定期執行、線程中斷。
 線程池幫我們解決了線程生命週期開銷資源不足的問題,通過重用線程,也提高了請求的響應速度

使用Java線程池的好處?
(1)重用存在的線程,減少對象創建、消亡的開銷,提升性能。
(2)可有效控制管理最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
(3)提供定時執行、定期執行、單線程、併發數控制等功能。

Java裏面線程池的頂級接口是Executor,是一個執行線程的工具,真正的線程池接口是ExecutorService

圖一:線程池的類體系結構

 

JDK 1.5以後,ThreadPoolExecutor作爲java.util.concurrent包對外提供基礎實現,以內部線程池的形式對外提供管理任務執行,線程調度,線程池管理等等服務。以下是其構造方法: 

圖二:線程池的工作流程圖

執行流程

    對於線程池的運行過程中,其中比較重要的幾個參數是:corePoolSize,maximumPoolSize,workQueue之間關係。如圖一所示,當一個新的任務請求到來時:
    1.當線程池小於corePoolSize時,新提交任務將創建一個新線程執行任務,即使此時線程池中存在空閒線程;
    2.當線程池達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行;
    3.當workQueue已滿,且maximumPoolSize>corePoolSize時,未達到最大的線程數,新提交任務會創建新線程執行任務;
    4.當提交任務數超過maximumPoolSize時,新提交任務由RejectedExecutionHandler處理;
    5.當線程池中超過corePoolSize線程,非核心線程空閒時間達到keepAliveTime時,關閉空閒線程;
    6.當設置allowCoreThreadTimeOut(true)時,線程池中核心線程空閒時間達到keepAliveTime也將關閉。

圖三:線程數量與阻塞隊列的關係

線程池排隊策略

    BlockingQueue是雙緩衝隊列。BlockingQueue內部使用兩條隊列,允許兩個線程同時向隊列一個存儲,一個取出操作。在保證併發安全的同時,提高了隊列的存取效率。常用的幾種BlockingQueue如下:
(1) 直接提交-SynchronousQueue
        直接提交-SynchronousQueue,直接提交策略時線程池不會對任務進行緩存,對於新提交的任務,如果線程池中沒有空閒的線程,就創建一個新的線程去處理,線程池具有無限增長的可能性。對於“脈衝式”流量請求的情況可能是致命的,對導致系統oom,或者線程數過多過度切換導致系統癱瘓;
(2) 有界隊列-ArrayBlockingQueue
    新提交的任務,當線程池中線程達到corePoolSize時,新進任務被放在隊列裏排隊等待處理。
    使用大型隊列+小型池:可以最大限度地降低 CPU 使用率、降低操作系統資源和上下文切換開銷,於此同時也降低吞吐量。如果任務頻繁的I/O繁阻塞,增加任務的耗時。
    使用小型隊列+大型池:CPU使用率較高;池子需要適量,否則容易出現oom或者線程的切換導致的系統崩潰。
(3) 無界隊列- LinkedBlockingQueue
   使用無界隊列將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize,此時maximumPoolSize 的值也就無效了。
(4) PriorityBlockingQueue:其所含對象的排序不是FIFO,而是依據對象的自然順序或者構造函數的Comparator決定。

拒絕策略

策略一:AbortPolicy:丟棄任務並拋出RejectedExecutionException異常【jdk默認策略】;
策略二:DiscardPolicy:直接丟棄新來的任務,隊列尾的任務,但是不拋出異常;
策略三:DiscardOldestPolicy:丟棄隊列最前面的任務,執行後面的任務;
策略四:CallerRunsPolicy:即不用線程池中的線程執行,在調用execute的線程裏面執行此command,會阻塞入口;

具體實現如下:

public class ThreadPoolRejectTest {
    private static Logger log = LoggerFactory.getLogger(ThreadPoolRejectTest.class);

    public static void main(String[] args) {
        //創建一個核心線程爲1,最大線程爲2,核心線程存活時間爲1s,有界隊列爲3的等待隊列;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1,2,1000,
                        TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(3));

        for (int i =0 ; i < 10 ; i++){
            ThreadTask task = new ThreadTask(i);
            threadPool.execute(task);
            log.info("線程池中的線程數:{}, 隊列中等待任務的線程數:{}, 已執行完的線程數:{}",threadPool.getCorePoolSize(),threadPool.getQueue().size(),threadPool.getCompletedTaskCount());
        }
        threadPool.shutdown();
    }
}

class ThreadTask implements Runnable {
    private static Logger log = LoggerFactory.getLogger(ThreadTask.class);

    private int taskNum;
    public ThreadTask(int num) {
        this.taskNum = num;
    }

    @Override
    public void run() {
        log.info("線程 {} 任務 {} 執行 完畢。", Thread.currentThread().getName(), taskNum);
        try {
            sleep(1);//這裏爲了效果明顯,必須模擬業務的停頓時間,否則執行太快看不到等待的效果;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用策略一:AbortPolicy

因爲jdk默認的就是策略一,所以默認運行結果和設置AbortPolicy策略一樣。

//創建一個核心線程爲1,最大線程爲2,核心線程存活時間爲1s,有界隊列爲3的等待隊列,採用默認的AbortPolicy 拒絕策略;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.AbortPolicy());

運行結果

圖四:AbortPolicy策略執行結果

從執行結果來看,任務任然執行完成了任務0-1-2-3-4 ,總過5個,任務5-6-7-8-9被拋棄了。AbordPolicy策略是,線程達到最大核心線程1個pool-1-thread-1時,放入隊列,隊列滿,又創建了pool-1-thread-2,此時新提交的任務將會直接丟棄,且拋出RejectedExecutionException異常。

使用策略二:DiscardPolicy

圖五:DiscardPolicy策略執行結果

從結果來看,任務任然執行完成了任務0-1-2-3-4-5 ,總過6個,任務6-7-8-9被拋棄了。但是和策略一不同的地方是沒有拋出拒絕異常,且丟棄的是後來最新提交的任務。
使用策略三:DiscardOldestPolicy

        //創建一個核心線程爲1,最大線程爲2,核心線程存活時間爲1s,有界隊列爲3的等待隊列,採用DiscardOldestPolicy拒絕策略;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.DiscardOldestPolicy());
圖六:DiscardOldestPolicy策略執行結果

從結果來看,線程池執行來0-1-5-7-8-9任務,拋棄來2-3-4-6四個任務,沒有拋出異常且丟棄的是隊列中老的請求

策略四:CallerRunsPolicy

//創建一個核心線程爲1,最大線程爲2,核心線程存活時間爲1s,有界隊列爲3的等待隊列,採用CallerRunsPolicy拒絕策略;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.CallerRunsPolicy());
圖七:CallerRunsPolicy策略執行結果

從結果來看,所有的10個任務全部執行,當隊列滿時,不想放棄執行任務但是由於池中已經沒有任何資源了,那麼就直接使用調用該execute的線程本身main來執行。同時也減緩來請求的提交速度,達到來反控的目的。

Executors的四種線程池

 newFixedThreadPool,構造一個固定線程數目的線程池,配置的corePoolSize與maximumPoolSize大小相同,同時使用了一個無界LinkedBlockingQueue存放阻塞任務,因此多餘的任務將存在再阻塞隊列,不會由RejectedExecutionHandler處理。

public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(
         nThreads, 
         nThreads,
         0L, 
         TimeUnit.MILLISECONDS,
         new LinkedBlockingQueue<Runnable>());
 }

newCachedThreadPool,構造一個緩衝功能的線程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一個無容量的阻塞隊列 SynchronousQueue,因此任務提交之後,將會創建新的線程執行;線程空閒超過60s將會銷燬。

public static ExecutorService newCachedThreadPool() {
 return new ThreadPoolExecutor(
        0, 
        Integer.MAX_VALUE,
        60L,
        TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
 }

newSingleThreadExecutor,構造一個只支持一個線程的線程池,配置corePoolSize=maximumPoolSize=1,無界阻塞隊列LinkedBlockingQueue;保證任務由一個線程串行執行。

public static ExecutorService newSingleThreadExecutor() {
 return new FinalizableDelegatedExecutorService(
            new ThreadPoolExecutor(
            1,
            1,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()));
 }

ScheduledThreadPoolExecutor,構造有定時功能的線程池,配置corePoolSize,無界延遲阻塞隊列DelayedWorkQueue;有意思的是:maximumPoolSize=Integer.MAX_VALUE,由於DelayedWorkQueue是無界隊列,所以這個值是沒有意義的。

public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {
 super(corePoolSize, 
       Integer.MAX_VALUE, 
       0,
       TimeUnit.NANOSECONDS,
       new DelayedWorkQueue(), 
       threadFactory);
 }

使用實例:

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
//延遲3秒後執行任務;
scheduledThreadPool.schedule(new ThreadTask(1),1,TimeUnit.SECONDS);
//延遲1秒後,每3秒執行一次;
scheduledThreadPool.scheduleAtFixedRate(new ThreadTask(2),1,3,TimeUnit.SECONDS);

CompletionService

      如果你向Executor提交了一個批處理任務,並且希望在它們完成後獲得結果。爲此你可以將每個任務的Future保存進一個集合,然後循環這個集合調用Future的get()取出數據,但是但獲取方式確實阻塞的,根據添加到線程池中的線程順序,依次獲取,獲取不到就阻塞,爲了解決這種情況,也可以採用輪詢的做法。幸運的是CompletionService幫你做了這件事情。CompletionService整合了Executor和BlockingQueue的功能。提交給ExecutorCompletionService的任務,會被封裝成一個QueueingFuture(一個FutureTask子類),此類的唯一作用就是在done()方法中,增加了將執行的FutureTask加入了內部隊列,此時外部調用者,就可以take到相應的執行結束的任務,其take方法返回已完成的一個Callable任務對應的Future對象,然後通過get就可以拿到我們想要的數據了。 CompletionService的take返回的future是哪個先完成就先返回哪一個,而不是根據提交順序。

CompletionService接口定義了一組任務管理接口:

  • submit() - 提交任務
  • take() - 獲取任務結果
  • poll() - 獲取任務結果

ExecutorCompletionService類是CompletionService接口的實現:
ExecutorCompletionService內部管理者一個已完成任務的阻塞隊列;
ExecutorCompletionService引用了一個Executor, 用來執行任務;
submit()方法最終會委託給內部的executor去執行任務;
take/poll方法的工作都委託給內部的已完成任務阻塞隊列;
如果阻塞隊列中有已完成的任務, take方法就返回任務的結果, 否則阻塞等待任務完成
poll與take方法不同, poll有兩個版本:

  • 無參的poll方法 --- 如果完成隊列中有數據就返回, 否則返回null
  • 有參數的poll方法 --- 如果完成隊列中有數據就直接返回, 否則等待指定的時間, 到時間後如果還是沒有數據就返回null

ExecutorCompletionService主要用與管理異步任務 (有結果的任務, 任務完成後要處理結果)

具體實例:

    public static void main(String[] args) throws InterruptedException {
        //自定義線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3000,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
        //使用CompletionService實現任務
        CompletionService completionService = new ExecutorCompletionService<Integer>(threadPool);

        //提交任務
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            //TODO 驗證等所有的任務提交後才能獲取,這點需要注意;容易阻塞大量的任務,隊列過大容易引起OOM;
            if(i == 4){
                sleep(4000);
            }
            completionService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    //TODO 驗證非阻塞的獲取,與Future+ Callable對比;
                    if (finalI == 3){
                        sleep(3000);
                    }
                    return finalI;
                }
            });
        }

        //獲取結果
        for (int i = 0; i < 5; i++) {
            try {
                log.info("批量處理任務後返回的結果:{}", completionService.take().get());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        //關閉線程池
        threadPool.shutdown();
    }

運行結果:

圖八: CompletionService執行結果

 從上圖可以看出,i=3的始終是最後執行完,通過CompletionService獲取的結果是非阻塞的,那個任務先返回就返回那個。

線程池監控

如果系統大量使用線程池,且請求量較大,需要使用線程池的監控,更快的定位問題,更好的掌握系統的性能。具體有以下幾個常用呢的參數需要注意:

  • taskCount:線程池需要執行的任務數量。
  • completedTaskCount:線程池在運行過程中已完成的任務數量,小於或等於taskCount。
  • largestPoolSize:線程池裏曾經創建過的最大線程數量。通過這個數據可以知道線程池是否曾經滿過。如該數值等於線程池的最大大小,則表示線程池曾經滿過。
  • getPoolSize:線程池的線程數量。如果線程池不銷燬的話,線程池裏的線程不會自動銷燬,所以這個大小隻增不減。
  • getActiveCount:獲取活動的線程數。
 @PostConstruct
    public void init() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            /**
             * 線程池需要執行的任務數
             */
            long taskCount = threadPoolExecutor.getTaskCount();
            /**
             * 線程池在運行過程中已完成的任務數
             */
            long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
            /**
             * 曾經創建過的最大線程數
             */
            long largestPoolSize = threadPoolExecutor.getLargestPoolSize();
            /**
             * 線程池裏的線程數量
             */
            long poolSize = threadPoolExecutor.getPoolSize();
            /**
             * 線程池裏活躍的線程數量
             */
            long activeCount = threadPoolExecutor.getActiveCount();

            log.info("async-executor monitor. taskCount:{}, completedTaskCount:{}, largestPoolSize:{}, poolSize:{}, activeCount:{}",
                    taskCount, completedTaskCount, largestPoolSize, poolSize, activeCount);
        }, 0, 10, TimeUnit.MINUTES);
    }

小結

自定義線程池需要根據業務的特性來決定,可以從以下幾個角度來分析:
1、任務的性質:CPU密集型任務、IO密集型任務和混合型任務。

  •  CPU密集型任務應配置儘可能小的線程,如配置Ncpu+1個線程的線程池;
  • 由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,最大線程數一般設爲2Ncpu+1最好;
  • 混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。
  • 如果這兩個任務執行時間相差太大,則沒必要進行分解。

2、任務的優先級:高、中和低。
   根據優先級可依使用優先級隊列;例如保證任務處理的順序性;
3、任務的執行時間:長、中和短。
   任務執行的時間比較長,不是cpu密集型,可以適當的增大線程數;
4、任務的依賴性:是否依賴其他系統資源,如數據庫連接。
    看任務場景,任務量不大可採取無界隊列,如果任務量非常大,要用有界隊列,有界隊列能增加系統的穩定性和預警能力,防止產生過多的線程導致OOM及系統不可用;如果有依賴數據庫的情況,處理比較耗時,可以適當增大線程數,更好的利用cpu;
ps:如果要獲取任務執行結果,用CompletionService,但是注意,獲取任務的結果的要重新開一個線程獲取,如果在主線程獲取,就要等任務都提交後才獲取,就會阻塞大量任務結果,隊列過大OOM,所以最好異步開個線程獲取結果。

 

資料參考:
https://blog.csdn.net/qq_22771739/article/details/81462059
https://blog.csdn.net/wang_rrui/article/details/78541786
https://blog.csdn.net/xu__cg/article/details/52962991
https://blog.csdn.net/zhh1072773034/article/details/74240897
https://blog.csdn.net/xu__cg/article/details/52962991

 

 

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