每個(gè)程序員都知道,多線程能提高應(yīng)用吞吐量和處理速度。但不是每個(gè)程序員都知道為什么?
CPU運(yùn)行時(shí),通過將于運(yùn)行時(shí)間分片,通過調(diào)度來分配給各個(gè)進(jìn)程線程來執(zhí)行。因?yàn)闀r(shí)間片非常短,所以常常讓人誤以為是多個(gè)線程是同時(shí)并行執(zhí)行。
使用多線程來提高程序處理速度,其本質(zhì)是提高對CPU的利用率。主要是兩個(gè)方面
- 柱塞等待時(shí)充分利用CPU
當(dāng)程序發(fā)生阻塞的操作時(shí)候,例如IO等待,CPU將就空閑下來了。而使用多線程,當(dāng)一些線程發(fā)生阻塞的時(shí)候,另一些線程則仍能利用CPU,而不至于讓CPU一直空閑。 - 利用CPU的多核并行計(jì)算能力
現(xiàn)在的CPU基本上都是多核的。使用多線程,可以利用多核同時(shí)執(zhí)行多個(gè)線程,而不至于單線程時(shí)一個(gè)核心滿載,而其他核心空閑。
多線程就一定能提高處理速度嗎?顯示著不一定。當(dāng)程序偏計(jì)算型的時(shí)候,盲目啟動(dòng)大量線程來并發(fā),并不能提高處理速度,反而會(huì)降低處理速度。因?yàn)樵诙鄠€(gè)線程進(jìn)行切換執(zhí)行的時(shí)候會(huì)帶會(huì)一定的開銷。其中有 上下文切換開銷,CPU調(diào)度線程的開銷,線程創(chuàng)建和消亡的開銷等。其中主要是上下文切換帶來的開銷。
上下文切換的開銷,主要是來自于當(dāng)線程切換時(shí)保存上一個(gè)線程現(xiàn)場和載入下一個(gè)線程現(xiàn)場的操作。
使用空循環(huán)來模擬計(jì)算性任務(wù),看下在不同數(shù)量的線程,程序的表現(xiàn)
import java.util.ArrayList;
import java.util.List;
public class CounterDemo {
private static final long num = 1000000000L;
public static void splitCount(int threadNum) {
for (long i = 0; i < num/threadNum; i++) {}
}
public static long getIntervalTimeToNow(long startTime) {
return System.currentTimeMillis() - startTime;
}
public static void main(String[] args) throws InterruptedException {
countWithMultithread(1);
countWithMultithread(10);
countWithMultithread(100);
countWithMultithread(1000);
countWithMultithread(10000);
}
private static void countWithMultithread(final int threadNum) throws InterruptedException {
long startTime;
Runnable splitCount = new Runnable() {
@Override
public void run() {
CounterDemo.splitCount(threadNum);
}
};
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadNum; i++) {
Thread thread1 = new Thread(splitCount);
list.add(thread1);
}
startTime = System.currentTimeMillis();
for (Thread th: list) {
th.start();
}
for (Thread th: list) {
th.join();
}
System.out.println(String.format("%1$9d", threadNum) + " thread need:"+String.format("%1$6d",getIntervalTimeToNow(startTime)));
}
}
輸出結(jié)果
1 thread need: 409
10 thread need: 92
100 thread need: 140
1000 thread need: 226
10000 thread need: 978
100000 thread need: 10059
執(zhí)行的時(shí)候,使用vmstat 查看 CS (context switch)切換次數(shù)
vmstat 1
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 2701584 151472 9676072 0 0 0 60 393 419 0 0 100 0 0
0 0 0 2701708 151472 9676084 0 0 0 0 751 751 1 0 99 0 0
0 0 0 2701708 151472 9676092 0 0 0 0 354 384 0 0 100 0 0
0 0 0 2701708 151472 9676096 0 0 0 0 435 464 0 0 100 0 0
0 0 0 2701708 151472 9676108 0 0 0 72 439 491 0 0 100 0 0
2 0 0 2701708 151472 9676128 0 0 0 32 381 453 0 0 100 0 0
1 0 0 2680344 151472 9676168 0 0 0 4 6077 4869 21 1 78 0 0
4 0 0 2664740 151472 9676180 0 0 0 0 126656 149528 17 11 72 0 0
3 0 0 2564376 151472 9676188 0 0 0 0 107107 138418 12 11 77 0 0
3 0 0 2565556 151472 9676196 0 0 0 0 128143 166234 7 14 79 0 0
4 0 0 2563056 151472 9676204 0 0 0 64 125162 163707 7 13 79 0 0
3 0 0 2567808 151472 9676208 0 0 0 0 136266 180092 7 13 80 0 0
2 0 0 2566560 151472 9676216 0 0 0 0 117768 154666 7 14 79 0 0
1 0 0 2568276 151472 9676220 0 0 0 0 107585 139240 8 13 79 0 0
當(dāng)上下文切換最多的時(shí)候每秒切換了18W+次。從輸入結(jié)果來看,線程1K,10K的時(shí)候,多線程并沒有打來處理效率的提升,反而下降了。
引起上下文切換的原因有哪些?主要有以下幾種:
- 當(dāng)前任務(wù)的時(shí)間片用完之后,系統(tǒng)CPU正常調(diào)度下一個(gè)任務(wù);
- 當(dāng)前任務(wù)碰到IO阻塞,調(diào)度線程將掛起此任務(wù),繼續(xù)下一個(gè)任務(wù);
- 多個(gè)任務(wù)搶占鎖資源,當(dāng)前任務(wù)沒有搶到,被調(diào)度器掛起,繼續(xù)下一個(gè)任務(wù);
- 用戶代碼掛起當(dāng)前任務(wù),讓出CPU時(shí)間;
- 硬件中斷;