背景:面試中會要求對5中線程池作分析。所以要熟知線程池的運行細節,如CachedThreadPool會引發oom嗎?
可選擇的阻塞隊列BlockingQueue詳解
首先看一下新任務進入時線程池的執行策略:
如果運行的線程少於corePoolSize,則 Executor始終首選添加新的線程,而不進行排隊。(如果當前運行的線程小於corePoolSize,則任務根本不會存入queue中,而是直接運行)
如果運行的線程大於等於 corePoolSize,則 Executor始終首選將請求加入隊列,而不添加新的線程。
如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出 maximumPoolSize,在這種情況下,任務將被拒絕。
主要有3種類型的BlockingQueue:
無界隊列
隊列大小無限制,常用的為無界的LinkedBlockingQueue,使用該隊列做為阻塞隊列時要尤其當心,當任務耗時較長時可能會導致大量新任務在隊列中堆積最終導致OOM。最近工作中就遇到因為采用LinkedBlockingQueue作為阻塞隊列,部分任務耗時80s+且不停有新任務進來,導致cpu和內存飆升服務器掛掉。
有界隊列
常用的有兩類,一類是遵循FIFO原則的隊列如ArrayBlockingQueue與有界的LinkedBlockingQueue,另一類是優先級隊列如PriorityBlockingQueue。PriorityBlockingQueue中的優先級由任務的Comparator決定。
使用有界隊列時隊列大小需和線程池大小互相配合,線程池較小有界隊列較大時可減少內存消耗,降低cpu使用率和上下文切換,但是可能會限制系統吞吐量。
同步移交
如果不希望任務在隊列中等待而是希望將任務直接移交給工作線程,可使用SynchronousQueue作為等待隊列。SynchronousQueue不是一個真正的隊列,而是一種線程之間移交的機制。要將一個元素放入SynchronousQueue中,必須有另一個線程正在等待接收這個元素。只有在使用無界線程池或者有飽和策略時才建議使用該隊列。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
在newCachedThreadPool中如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
初看該構造函數時我有這樣的疑惑:核心線程池為0,那按照前面所講的線程池策略新任務來臨時無法進入核心線程池,只能進入 SynchronousQueue中進行等待,而SynchronousQueue的大小為1,那豈不是第一個任務到達時只能等待在隊列中,直到第二個任務到達發現無法進入隊列才能創建第一個線程?
這個問題的答案在上面講SynchronousQueue時其實已經給出了,要將一個元素放入SynchronousQueue中,必須有另一個線程正在等待接收這個元素。因此即便SynchronousQueue一開始為空且大小為1,第一個任務也無法放入其中,因為沒有線程在等待從SynchronousQueue中取走元素。因此第一個任務到達時便會創建一個新線程執行該任務。
這里引申出一個小技巧:有時我們可能希望線程池在沒有任務的情況下銷毀所有的線程,既設置線程池核心大小為0,但又不想使用SynchronousQueue而是想使用有界的等待隊列。顯然,不進行任何特殊設置的話這樣的用法會發生奇怪的行為:直到等待隊列被填滿才會有新線程被創建(ps 大於等待隊列小於最大線程池),任務才開始執行。這並不是我們希望看到的,此時可通過allowCoreThreadTimeOut使等待隊列中的元素出隊被調用執行,詳細原理和使用將會在后續博客中闡述。
newFixedThreadPool 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
newWorkStealingPool創建一個擁有多個任務隊列(以便減少連接數)的線程池
使用無界隊列的線程池會導致內存飆升嗎?
ps:以圖文的形式講解線程池的運行過程。OOM會發生在無界隊列
當出現workQueue里不斷的積壓越來越多得任務,不停的增加。
這個過程中會導致機器的內存使用不停的飆升,最后也許極端情況下就導致JVM OOM了,系統就掛掉了。
阻塞隊列中任務過多會導致OOM異常。
線程過多會導致上下文切換的開銷、消耗cpu資源
實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。
我們詳細的解釋一下為什么要使用線程池?
在java中,如果每個請求到達就創建一個新線程,開銷是相當大的。在實際使用中,創建和銷毀線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創建和銷毀線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個jvm里創建太多的線程,可能會使系統由於過度消耗內存或“切換過度”而導致系統資源不足。為了防止資源不足,需要采取一些辦法來限制任何給定時刻處理的請求數目,盡可能減少創建和銷毀線程的次數,特別是一些資源耗費比較大的線程的創建和銷毀,盡量利用已有對象來進行服務。
線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務重復使用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使用應用程序響應更快。另外,通過適當的調整線程中的線程數目可以防止出現資源不足的情況。
