OOM分析之問題定位(二)

一.背景

上一篇OOM分析之問題定位(一)中講到經過單例模式能夠有效的減小內存使用。可是隨着壓測併發數的不斷提升,QRCodeTask對象不斷增長,內存佔用相應也會一直增長。再加上QRCodeTask任務的業務功能是合成圖片,屬於CPU密集型任務。若是處理的QRCodeTask任務太多,會一直佔用CPU,形成其它接口響應的速度變慢。html

所以能夠對ThreadPoolExecutor深刻研究來找到進一步優化的措施。java

Java SE API文檔連接以下:程序員

docs.oracle.com/javase/7/do…api

二.ThreadPoolExecutor介紹

1.構造函數解析

經過查看源碼能夠看到全部不一樣形參的構造函數最終都會調用到如下的構造函數。數組

public ThreadPoolExecutor(int corePoolSize, 
              int maximumPoolSize,    
              long keepAliveTime, 
              TimeUnit unit, 
              BlockingQueue workQueue,
              ThreadFactory threadFactory, 
              RejectedExecutionHandler handler)
複製代碼

2.參數介紹

corePoolSize:線程池中核心線程數目,即便空閒也不回收,除非設置了allowCoreThreadTimeOut時間。當有新增任務且當前線程數小於corePoolSize時,會繼續建立核心線程執行任務。當線程數達到corePoolSize時,後面新增任務都會加入到BlockingQueue隊列中等待執行。緩存

maximumPoolSize:線程池中永許達到的最大線程數母。若是BlockingQueue隊列滿,且當前線程數小於maximumPoolSize,則線程池會建立新的臨時線程繼續執行後續任務,直到線程數目達到maximumPoolSize。若是BlockingQueue使用了無界隊列,此參數設置了也沒有實際意義。bash

keepAliveTime:臨時線程空閒後的存活時間,超時後空閒線程會被銷燬。併發

unit:keepAliveTime的時間單元。單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS)和毫微秒(NANOSECONDS)。oracle

workQueue:暫存任務的工做隊列,執行execute方法提交的Runnable任務都會加入到此隊列中。與ThreadPoolExecutor相關的BlockingQueue接口實現類有如下4種:函數

  1. ArrayBlockingQueue:數組實現的有界隊列。

  2. LinkedBlockingQueue:鏈表實現的無界隊列,未指明容量時,默認大小爲Integer.MAX_VALUE,即21億多。

  3. SynchronousQueue:不能存儲元素的同步隊列,主要用於生產者消費者之間的同步。

  4. DelayedWorkQueue:延遲隊列,一個無界阻塞隊列,只有延遲時間結束才能出隊。

ThreadFactory:線程工廠,能夠給新建線程設置優先級、名字、是否守護線程、線程組等信息。

RejectedExecutionHandler:任務隊列滿且線程數也到達極限時的回調函數,用來對沒法處理的任務實施拒絕策略,默認策略是AbortPolicy。Executor提供的4種拒絕策略以下:

  1. ThreadPoolExecutor.AbortPolicy:默認策略,拋出java.util.concurrent.RejectedExecutionException異常 。

  2. ThreadPoolExecutor.CallerRunsPolicy: 調用execute()方法 ,重試添加當前的任務。

  3. ThreadPoolExecutor.DiscardPolicy:直接拋棄沒法處理的任務。

  4. ThreadPoolExecutor.DiscardOldestPolicy:拋棄隊列中最先加進去的任務,即隊列頭的任務,而後執行execute()方法,從新添加新提交的任務。

除以上4種策略外,ThreadPoolExecutor還能夠自定義飽和策略。用戶能夠靈活的根據實際應用場景實現RejectedExecutionHandler接口,添加符合自身要求的業務代碼,如記錄日誌或持久化不能處理的任務等。

3.ThreadPoolExecutor工做原理

當有新增任務且當前線程數小於corePoolSize時,建立核心線程執行任務。

當線程數達到corePoolSize時,後續新增任務都會加到BlockingQueue隊列中排隊等待執行。

當BlockingQueue也滿後,會建立臨時線程執行任務。若是臨時線程超過空閒時間後會被銷燬。

當線程總數達到maximumPoolSize時,後續新增任務都會被RejectedExecutionHandler拒絕。

三.Executors介紹

Java SE API文檔連接以下:

docs.oracle.com/javase/7/do…

爲了方便程序員的使用,Java設計者貼心的在Executors類中實現了4種獲取線程池的靜態方法,目的是讓程序員不關心各個參數的細節就能獲得合適本身的線程池。下面是對各個函數進行介紹:

1.newFixedThreadPool

固定大小線程池,無界隊列。

構造方法:

public static ExecutorService newFixedThreadPool(intnThreads){   
 return newThreadPoolExecutor(nThreads,
                              nThreads,                          
                              0L,
                              TimeUnit.MILLISECONDS,
                              newLinkedBlockingQueue());
 }
public ThreadPoolExecutor(int corePoolSize,
                         intmaximumPoolSize,                                               longkeepAliveTime,
                         TimeUnit unit,                                                                                 BlockingQueue workQueue){        
         this(corePoolSize, maximumPoolSize, keepAliveTime, 
   unit, workQueue,Executors.defaultThreadFactory(),defaultHandler);
}
複製代碼

能夠看到corePoolsize=maximumPoolSize,超時時間爲0,並用了無界任務隊列。當任務小於corePoolsize時,會直接建立新的線程執行新增任務。當任務數等於corePoolsize時,新增任務加到無界隊列中。此後全部線程即便空閒也不會回收,會一直保持活動狀態直到執行shutdown方法。

若是隨着任務的增長,任務隊列也滿,則執行默認的飽和策略,即拋出異常,進程中止。

2.newSingleThreadExecutor

單線程線程池,無界隊列。若是線程意外中止,會新建一個線程代替它去執行後續任務。能夠保證任務都是按序執行。

public staticExecutorService newSingleThreadExecutor() {   
        return newFinalizableDelegatedExecutorService(           
               new ThreadPoolExecutor(1, 
                                      1,
                                      0L,
                                      TimeUnit.MILLISECONDS, 
                                      newLinkedBlockingQueue()));
}
複製代碼

corePoolsize=maximumPoolSize=1,超時時間爲0,無界任務隊列。線程池中只有一個核心線程,當有新增任務時加到無界隊列中。

3.newCachedThreadPool

可緩存線程池,無界線程池。

public static ExecutorServicenewCachedThreadPool( 
ThreadFactory threadFactory) { 
    return new ThreadPoolExecutor(
                                0,
                                Integer.MAX_VALUE, 
                                60L,
                                TimeUnit.SECONDS, 
                                newSynchronousQueue(),
                                threadFactory); 
}
複製代碼

corePoolsize=0,maximumPoolSize=Integer.MAX_VALUE,超時時間爲60s,SynchronousQueue任務隊列。若處理任務大於當前線程數,且無空閒線程則新建線程執行任務。如有空閒線程,則重複利用空閒線程。當線程空閒時間超時,會對線程進行銷燬。

此線程池與其它線程池的最大不一樣點是SynchronousQueue隊列不能暫存任務,只能經過線程數的無限增長來處理併發任務。

4.newScheduledThreadPool

定時任務線程池,使用無界隊列。能夠定時以及週期性執行任務。

public staticScheduledExecutorService newScheduledThreadPool(
              int corePoolSize, 
              ThreadFactorythreadFactory) {       
   return newScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,                            ThreadFactory threadFactory) {     
     super(corePoolSize, 
     Integer.MAX_VALUE,
     0, 
     NANOSECONDS,
     new DelayedWorkQueue(),threadFactory);
 }
複製代碼

四.項目優化

經過上面的介紹,相信你們對ThreadPoolExecutor的工做機制有了深刻的瞭解,再回到項目中遇到的問題。

查看代碼發現項目中的線程池是使用了Executors.newFixedThreadPool,所以當請求持續增長時,QRCodeTask任務就會一直加到無界隊列中等待執行,即便經過單例模式使內存佔用獲得了優化,可是在併發量大的狀況下,內存也可能隨着隊列元素的無限增長最終致使被撐爆。

根據墨菲定律,若是事情有變壞的可能,無論這種可能性有多小,它總會發生。所以爲了不之後線上應用發生故障,必須對這部分代碼作進一步的優化。

此處咱們不使用 Executors 去獲得線程池,而是直接調用ThreadPoolExecutor構造函數,這樣能夠經過靈活設置參數來構造知足業務需求的線程池,避免資源耗盡。在此項目中修改以下:

ThreadPoolExecutor executor = newThreadPoolExecutor(
                              10, 
                              15, 
                              60, 
                              TimeUnit.MINUTES, 
                              new LinkedBlockingQueue(10000), 
                              new CustomThreadFactory(),  
                              newCustomRejectedExecutionHandler());
}
複製代碼

建立核心線程爲10,最大線程20,超時時間60s,任務隊列爲10000的線程池,而且使用了自定義的ThreadFactory和RejectedExecutionHandler。爲方便之後問題的追溯,在自定義ThreadFactory中定義了本身的線程名,並在RejectedExecutionHandle中實現了知足業務需求的處理方法。當請求任務隊列達到10000,線程數達到20時,執行給定的拒絕策略。

最後對改造後的程序進行充分測試,應用性能表現平穩,也符合了業務要求,至此這個問題獲得了圓滿解決。

推薦閱讀:

OOM分析之問題定位(一)

想要了解更多,關注公衆號:七分熟pizza

在這裏插入圖片描述
相關文章
相關標籤/搜索