深刻理解java線程池

線程池

在介紹線程池以前先看看一道面試題:爲何要使用線程池?使用線程池的優點是什麼?java

做用:面試

​ 線程池作的工做主要是控制運行的線程的數量,處理過程當中將任務放入隊列,而後再線程建立後啓動這些任務,若是線程數量超過了最大的數量的線程排隊等候,等其餘線程執行完畢,再從隊列中取出任務來執行。主要特定:線程複用,控制最大併發數,管理線程。在阿里巴巴java開發手冊中,使用線程池的好處是減小在建立和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。若是不使用線程池,有可能形成系統建立大量同類線程而致使消耗完內存或者 「過分切換」的問題。緩存

優點:數據結構

  1. 下降資源消耗,經過重複利用已經建立的線程下降線程建立和銷燬形成的消耗
  2. 提升響應速度,當任務到達時,任務能夠不須要等到線程建立就能當即執行。
  3. 提升線程的可管理性。線程時稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行同一的分配,調優和監控。

java線程池概述:Java中的線程池是經過Executor框架實現的,該框架用到了Executor,Executors,ExecutorService,ThreadPoolExecutor這幾個類,以下圖所示: 多線程

線程池經常使用建立方式

Executors.newFixedThreadPool(int)併發

​ 建立一個固定長度的線程池,不能夠擴容。能夠控制線程的最大併發數量,超出的線程會在隊列中等待。這種方式建立的線程池corePoolSize和maxinumPoolSize的值是相等的,它使用的是LinkedBlockingQueue。(注意: LinkedBlockingQueue默認大小是Integer.MAX_VALUE)框架

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                       //LinkedBlockingQueue默認大小是Integer.MAX_VALUE
                                      new LinkedBlockingQueue<Runnable>());
    }

Executors.newSingleThreadExecutor();this

​ 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序執行。這種方式建立建立的線程池將corePoolSize和maxinumPoolSize都設置爲1,它使用的是LinkedBlockingQueue。線程

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    //LinkedBlockingQueue默認大小是Integer.MAX_VALUE
                                    new LinkedBlockingQueue<Runnable>()));
    }

Executors.newCachedThreadPool();code

​ 建立一個可緩存線程池,若是線程池長度超過處理須要,能夠靈活回收空閒線程,若無可回收,則建立新線程這種方式建立的線程池將corePoolSize設置爲0,將maxinumPoolSize設置爲Integer.MAX_VALUE,使用的是SynchronousQueue,也就是說來了任務就建立線程運行,當線程空閒超過60秒就銷燬線程。它底層使用的是SynchronousQueue

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

總結:上面三種不管哪一種方式建立線程池底層都是用了ThreadPoolExecutor,底層的數據結構都是阻塞隊列。

線程池的簡單使用,模擬10我的到5個窗口辦理業務。

public class MyThreadPool {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 0; i < 10; i++) {
                pool.submit(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 處理業務");
                });

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }
}

線程池七大參數

線程池的建立源碼

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  1. corePoolSize:線程池中的常駐核心線程數。在建立線程池以後,當有請求任務來了以後,就會安排池中的線程去執行請求任務,近似理解爲今日當值線程。當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列中。
  2. maxinumPoolSize:線程池中可以容納同時執行的最大線程數,此值必須大於等於1
  3. keepAliveTime:多餘的空閒線程的存活時間。當前線程池數量超過corePoolSize時候,當空閒時間達到keepAliveTime值時候,多餘空閒線程會被銷燬直到剩下corePoolSize數量個線程位置。默認狀況下,只有當線程池中對額線程數大於corePoolSize時keepAliveTime纔會起做用,直到線程池中的線程數量不大於corePoolSize。
  4. unit:keepAliveTime的單位
  5. workQueue:任務隊列,被提交可是還沒有被執行的任務
  6. threadFactory:表示生成線程池中工做線程線程工廠,用於建立線程通常用默認便可
  7. handler:拒絕策略,表示當前隊列滿了而且線程大於或者等於線程池的最大線程數時如何來拒絕請求執行的runnable的策略

線程池原理

  1. 在建立了線程池後,等待提交過來的任務請求

  2. 當調用execute()方法添加一個請求任務時候,線程池會作以下判斷

    2.1 若是正在運行的線程數量<corePoolSize,立刻建立線程處理這個任務

    2.2 若是正在運行的線程數量>=corePoolSize,就將這個任務加入隊列

    2.3 若是這個時候隊列滿了並且正在運行線程數<maxinumPoolsize,就建立非核心線程馬上處理這個任務

    2.4 若是隊列滿了,並且正在運行線程數量>=maxinumPoolsize,那麼線程就會啓動飽和拒絕策略來執行

  3. 當一個線程完成任務時候,它會從隊列中取出下一個任務執行

  4. 當一個線程空閒超過keepAliveTime是,線程池會判斷若是當前運行的線程數量大於corePoolSize,那麼這個線程會被銷燬,因此線程池的全部任務完成以後它最終會收縮到corePoolSize的大小。

線程池的拒絕策略

所謂的拒絕策略就是線程池須要分配的線程數量大於線程池最大容量,沒法分配新的線程處理當前任務時候的策略。JDK默認提供瞭如下四種拒絕策略

AbortPolicy(默認):直接拋出RejectedExecutionExeception異常阻止系統運行

CallerRunsPolicy:「調用者運行」一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者

DiscardOldestPolicy:拋棄隊列中等待最久的任務,而後把當前任務加入隊列中嘗試再次提交當前任務

DiscardPolicy:直接多餘丟棄任務,不予任何處理也不會拋出異常。若是容許任務丟失,這是最好的解決方案。

這四種拒絕策略均實現了RejectedExecutionHandler接口

代碼驗證四種拒絕策略

因爲使用Executors建立的線程池workQueue隊列默認的長度是Integer.MAX_VALUE,這個數字太大,即便使用默認的拒絕策略也通常不會拋出相應的異常,反而可能形成OOM,因此阿里巴巴java技術手冊要求不能使用Executors的方式建立線程池,而是應該使用ThreadPoolExecutor的方式建立線程。

AbortPolicy策略

在自定義的線程池中maximumPoolSize=5,workQueue=3,因此該線程池可以接受的最大請求數爲8,而請求數倒是10,使用AbortPolicy拒絕策略超過8個請求就會拋出異常

public class MyThreadPool {
    public static void main(String[] args) {

        ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,                                       
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
				//建立自定義線程池,使用AbortPolicy拒絕策略,請求線程超過8就會拋出異常                               new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 10; i++) {
                int temp = i;
                pool.submit(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 處理業務" + temp);
                });

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }
}
                                                      
pool-1-thread-1	 處理業務0
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@b4c966a rejected from java.util.concurrent.ThreadPoolExecutor@2f4d3709[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
pool-1-thread-2	 處理業務1
pool-1-thread-1	 處理業務3
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
pool-1-thread-2	 處理業務4
pool-1-thread-2	 處理業務5
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
pool-1-thread-3	 處理業務2
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
pool-1-thread-4	 處理業務6
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
pool-1-thread-5	 處理業務7
	at com.ThreadPool.MyThreadPool.main(MyThreadPool.java:26)

CallerRunsPolicy策略

「調用者運行」一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者。在本例中超出的兩個請求會被交還給main線程處理

//將上述代碼建立線程池的拒絕策略改變,其餘不變 
ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());


pool-1-thread-1	 處理業務0
main	 處理業務8    //請求數爲10,超過的兩個請求交給調用者main線程處理
main	 處理業務9
pool-1-thread-2	 處理業務1
pool-1-thread-2	 處理業務3
pool-1-thread-2	 處理業務4
pool-1-thread-2	 處理業務5
pool-1-thread-4	 處理業務6
pool-1-thread-3	 處理業務2
pool-1-thread-5	 處理業務7

DiscardOldestPolicy

拋棄隊列中等待最久的任務,而後把當前任務加入隊列中嘗試再次提交當前任務。

ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());


pool-1-thread-1	 處理業務0
pool-1-thread-1	 處理業務5
pool-1-thread-1	 處理業務8
pool-1-thread-1	 處理業務9
pool-1-thread-2	 處理業務1
pool-1-thread-3	 處理業務2
pool-1-thread-4	 處理業務6
pool-1-thread-5	 處理業務7

執行結果中3和4請求被丟棄,由此可知該隊列中3和4請求時等待時間最久的

DiscardPolicy

直接多餘丟棄任務,不予任何處理也不會拋出異常。

ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

pool-1-thread-2	 處理業務1
pool-1-thread-2	 處理業務3
pool-1-thread-2	 處理業務4
pool-1-thread-2	 處理業務5
pool-1-thread-1	 處理業務0
pool-1-thread-3	 處理業務2
pool-1-thread-4	 處理業務6
pool-1-thread-5	 處理業務7

從執行結果來看請求8和請求9會被拋棄

合理配置線程池參數

如何合理配置線程池參數?

首先要獲取硬件參數,經過Runtime.getRuntime().availableProcessors()獲取CPU的核心數目

而後要區分所執行的任務是CPU密集型仍是IO密集型。
CPU密集型:該任務須要大量的運算,而沒有阻塞,CPU一直全速運行。 CPU密集任務只有在真正的多核CPU上纔可能獲得加速(經過多線程)。而在單核CPU上不管開幾個模擬的多線程,該任務都不可能獲得加速,由於CPU總的運算能力就是那麼多。CPU密集型任務配置儘量少的線程數量:通常公式:CPU核數+1線程的線程池

IO密集型:執行該任務須要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會致使浪費大量的CPU運算能力在等待上。因此在IO密集型任務中使用多線程能夠大大的加速程序的運行,即便在單核CPU上,這種加速就是利用了被浪費掉的阻塞時間。IO密集型大部分線程被阻塞,故須要多配置線程數。通常有如下兩種配置策略

  1. 因爲IO密集型任務線程並非一直在執行任務,則應該配置儘量多的線程,如CPU核數*2

  2. 參考公式:CPU核數 / (1 - 阻塞係數)【阻塞係數在0.8~0.9之間】

    好比8核CPU:線程池應配置8/(1-0.9)= 80個線程數。

注意事項

阿里巴巴java開發手冊v1.2中對線程池中有如下兩點注意事項

【強制】線程資源必須經過線程池提供,不容許在應用中自行顯式建立線程。

說明:使用線程池的好處是減小在建立和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。若是不使用線程池,有可能形成系統建立大量同類線程而致使消耗完內存或者 「過分切換」的問題。

【強制】線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。

說明:Executors 返回的線程池對象的弊端以下:

  1. FixedThreadPool 和 SingleThreadPool: 容許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而致使 OOM。

  2. CachedThreadPool 和 ScheduledThreadPool: 容許的建立線程數量爲 Integer.MAX_VALUE,可能會建立大量的線程,從而致使 OOM。

相關文章
相關標籤/搜索