執行緒的建立及執行緒池

塵虛緣_KY發表於2017-05-07

 

目錄

執行緒的建立

繼承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

 

 

相關文章