熟悉java多線程的朋友一定十分了解java的線程池,jdk中的核心實作類為java.util.concurrent.ThreadPoolExecutor。大家可能了解到它的原理,甚至看過它的源碼;但是就像我一樣,大家可能對它的作用存在誤解。現在問題來了,jdk為什麼要提供java線程池?使用java線程池對于每次都建立一個新Thread有什麼優勢?
對線程池的誤解
很長一段時間裡我一直以為java線程池是為了提高多線程下建立線程的效率。建立好一些線程并緩存線上程池裡,後面來了請求(Runnable)就從連接配接池中取出一個線程處理請求;這樣就避免了每次建立一個新Thread對象。直到前段時間我看到一篇Neal Gafter(和Joshua Bloch合著了《Java Puzzlers》,現任職于微軟,主要從事.NET語言方面的工作)的訪談,裡面有這麼一段談話(http://www.infoq.com/cn/articles/neal-gafter-on-java):
乍一看,大神的思路就是不一樣:java線程池是為了防止java線程占用太多資源?
雖然是java大神的訪談,但是也不能什麼都信,你說占資源就占資源?還是得寫測試用例測一下。
首先驗證下我的了解:
java線程池和建立java線程哪個效率高?
直接上測試用例:
public class ThreadPoolTest extends TestCase {
private static final int COUNT = 10000;
public void testThreadPool() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(COUNT);
ExecutorService executorService = Executors.newFixedThreadPool(100);
long bg = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
Runnable command = new TestRunnable(countDownLatch);
executorService.execute(command);
}
countDownLatch.await();
System.out.println("testThreadPool:" + (System.currentTimeMillis() - bg));
}
public void testNewThread() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(COUNT);
long bg = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
Runnable command = new TestRunnable(countDownLatch);
Thread thread = new Thread(command);
thread.start();
}
countDownLatch.await();
System.out.println("testNewThread:" + (System.currentTimeMillis() - bg));
}
private static class TestRunnable implements Runnable {
private final CountDownLatch countDownLatch;
TestRunnable(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
countDownLatch.countDown();
}
}
}
這裡使用Executors.newFixedThreadPool(100)是為了控制線程池的核心連接配接數和最大連接配接數一樣大,都為100。
我的機子上的測試結果:
testThreadPool:31
testNewThread:624
可以看到,使用線程池處理10000個請求的處理時間為31ms,而每次啟用新線程的處理時間為624ms。
好了,使用線程池确實要比每次都建立新線程要快一些;但是testNewThread一共耗時624ms,算下平均每次請求的耗時為:
624ms/10000=62.4us
每次建立并啟動線程的時間為62.4微秒。根據80/20原理,這點兒時間根本可以忽略不計。是以線程池并不是為了效率設計的。
java線程池是為了節約資源?
再上測試用例:
public class ThreadPoolTest extends TestCase {
public void testThread() throws InterruptedException {
int i = 1;
while (true) {
Runnable command = new TestRunnable();
Thread thread = new Thread(command);
thread.start();
System.out.println(i++);
}
}
private static class TestRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上用例模拟每次請求都建立一個新線程處理請求,然後預設每個請求的處理時間為1000ms。而在我的機子上當請求數達到1096時會記憶體溢出:
java.lang.OutOfMemoryError: unable to create new native thread
為什麼會抛OOM Error呢?因為jvm會為每個線程配置設定一定記憶體(JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K,也可以通過jvm參數-Xss來設定),是以當線程數達到一定數量時就報了該error。
設想如果不使用java線程池,而為每個請求都建立一個新線程來處理該請求,當請求量達到一定數量時一定會記憶體溢出的;而我們使用java線程池的話,線程數量一定會<=maximumPoolSize(線程池的最大線程數),是以設定合理的話就不會造成記憶體溢出。
現在問題明朗了:java線程池是為了防止記憶體溢出,而不是為了加快效率。
淺談java線程池
上文介紹了java線程池啟動太多會造成OOM,使用java線程池也應該設定合理的線程數數量;否則應用可能十分不穩定。然而該如何設定這個數量呢?我們可以通過這個公式來計算:
(MaxProcessMemory – JVMMemory – ReservedOsMemory) / (ThreadStackSize) = Max number of threads
MaxProcessMemory 程序最大的記憶體
JVMMemory JVM記憶體
ReservedOsMemory JVM的本地記憶體
ThreadStackSize 線程棧的大小
MaxProcessMemory
MaxProcessMemory:程序最大的尋址空間,當然也不能超過虛拟記憶體和實體記憶體的總和。關于不同系統的程序可尋址的最大空間,可參考下面表格:
Maximum Address Space Per Process | |
Operating System | |
Redhat Linux 32 bit | 2 GB |
Redhat Linux 64 bit | 3 GB |
Windows 98/2000/NT/Me/XP | |
Solaris x86 (32 bit) | 4 GB |
Solaris 32 bit | |
Solaris 64 bit | Terabytes |
JVMMemory
JVMMemory: Heap + PermGen,即堆記憶體和永久代記憶體和(注意,不包括本地記憶體)。
ReservedOsMemory
ReservedOSMemory:Native heap,即JNI調用方法所占用的記憶體。
ThreadStackSize
ThreadStackSize:線程棧的大小,JDK5.0以後每個線程堆棧大小預設為1M,以前每個線程堆棧大小為256K;可以通過jvm參數-Xss來設定;注意-Xss是jvm的非标準參數,不強制所有平台的jvm都支援。
如何調大線程數?
如果程式需要大量的線程,現有的設定不能達到要求,那麼可以通過修改MaxProcessMemory,JVMMemory,ThreadStackSize這三個因素,來增加能建立的線程數:
MaxProcessMemory 使用64位作業系統
JVMMemory 減少JVMMemory的配置設定
ThreadStackSize 減小單個線程的棧大小
熬夜不易,點選請老王喝杯烈酒!!!!!!!