OOM分析之問題定位(二)

七分熟pizza發表於2019-03-18

一.背景

上一篇OOM分析之問題定位(一)中講到通過單例模式可以有效的減少記憶體使用。但是隨著壓測併發數的不斷提高,QRCodeTask物件不斷增加,記憶體佔用相應也會一直增加。再加上QRCodeTask任務的業務功能是合成圖片,屬於CPU密集型任務。如果處理的QRCodeTask任務太多,會一直佔用CPU,造成其它介面響應的速度變慢。

因此可以對ThreadPoolExecutor深入研究來找到進一步優化的措施。

Java SE API文件連結如下:

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

二.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使用了無界佇列,此引數設定了也沒有實際意義。

keepAliveTime:臨時執行緒空閒後的存活時間,超時後空閒執行緒會被銷燬。

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

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

在這裡插入圖片描述

相關文章