案例
最近在學習Java調優,有個案例是ThraadPool導致OOM,在不瞭解線程池的情況很難看出問題來。
代碼片
.
package com.example.demo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPool {
public static void main(String[] arr) {
ExecutorService executorService = new ThreadPoolExecutor(1, 2,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (; ; ) {
Person person = new Person();
executorService.execute(() -> {
person.doingSomething();
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Person {
public String name;
public Integer sex;
public String age;
public void doingSomething() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
這段代碼很簡單,創建一個線程很少,使用一個無界緩存隊列的線程池,然後頻繁的給線程池加執行任務,很顯然,在jvm內存設置的不夠大的情況,拋出outOfMemory是遲早的事情,只是這個時間有點長。
不信執行 java -Xms20M -Xmx20M -XX:+PrintGC com.example.demo.TestThreadPool 試試
爲了快速試錯,將堆大小改成20M。
下面花了4個小時終於等到錯誤結果了:
爲什麼會OOM
我們來分析一下,首先線程池執行execute時,並不是馬上執行任務,它是先把它插入線程池裏面的隊列,然後排隊由線程池的線程執行。你可以把它想象成一個蓄水池,需要執行的任務是水,池子一邊進水,一邊放水,什麼情況下會發生水的溢出呢?很簡單,進水的速度比放水的速度還快時,水就會溢出。我們再來回到代碼,上面的代碼每100毫秒就會加一個執行任務,那麼一秒鐘的時間它加入任務數是1000/100=10個,然後這個線程池最多有兩個執行線程,每個線程執行的時間需要3秒,那它1秒鐘能釋放的任務數是2*1000/3000=2/3個,10>2/3,進來的速度明顯比釋放的快多了,所以它任務隊列是一直增長的,在任務裏面需要使用的對象,jvm是不會把它回收的,所以上面的例子中,創建person實例而不被gc回收的數量會越來越多,最終會把堆內存撐爆。
那麼要怎麼樣才能解決這個問題呢?這個沒有100%可以保證解決的辦法,但是可以從兩個方面去處理,要麼減少加入的任務數量(但是很多程序它的壓力就是那麼大的沒法優化,可能只能想其他辦法,通過負載均衡加多個更多的服務),要麼增加線程數量(線程數也不是越多越好的),那如果採用增加線程的方式,那要加多少線程纔夠呢?這個問題就要需要用到小學數學知識了,任務增加的速率爲1000/100=10,一個線程處理的任務的速率是1*1000/3000=1/3,這個線程池一共至少需要10/(1/3)=30個線程,才能消化掉這個創建任務速度。