每个程序员都知道,多线程能提高应用吞吐量和处理速度。但不是每个程序员都知道为什么?
CPU运行时,通过将于运行时间分片,通过调度来分配给各个进程线程来执行。因为时间片非常短,所以常常让人误以为是多个线程是同时并行执行。
使用多线程来提高程序处理速度,其本质是提高对CPU的利用率。主要是两个方面
- 柱塞等待时充分利用CPU
当程序发生阻塞的操作时候,例如IO等待,CPU将就空闲下来了。而使用多线程,当一些线程发生阻塞的时候,另一些线程则仍能利用CPU,而不至于让CPU一直空闲。 - 利用CPU的多核并行计算能力
现在的CPU基本上都是多核的。使用多线程,可以利用多核同时执行多个线程,而不至于单线程时一个核心满载,而其他核心空闲。
多线程就一定能提高处理速度吗?显示着不一定。当程序偏计算型的时候,盲目启动大量线程来并发,并不能提高处理速度,反而会降低处理速度。因为在多个线程进行切换执行的时候会带会一定的开销。其中有 上下文切换开销,CPU调度线程的开销,线程创建和消亡的开销等。其中主要是上下文切换带来的开销。
上下文切换的开销,主要是来自于当线程切换时保存上一个线程现场和载入下一个线程现场的操作。
使用空循环来模拟计算性任务,看下在不同数量的线程,程序的表现
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)));
}
}
输出结果
1 thread need: 409
10 thread need: 92
100 thread need: 140
1000 thread need: 226
10000 thread need: 978
100000 thread need: 10059
执行的时候,使用vmstat 查看 CS (context switch)切换次数
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
当上下文切换最多的时候每秒切换了18W+次。从输入结果来看,线程1K,10K的时候,多线程并没有打来处理效率的提升,反而下降了。
引起上下文切换的原因有哪些?主要有以下几种:
- 当前任务的时间片用完之后,系统CPU正常调度下一个任务;
- 当前任务碰到IO阻塞,调度线程将挂起此任务,继续下一个任务;
- 多个任务抢占锁资源,当前任务没有抢到,被调度器挂起,继续下一个任务;
- 用户代码挂起当前任务,让出CPU时间;
- 硬件中断;