1.多线程一定好吗
1.1 适用场景
- 阻塞等待时充分利用CPU
当程序发生阻塞的操作时候,例如IO等待,CPU将就空闲下来了。而使用多线程,当一些线程发生阻塞的时候,另一些线程则仍能利用CPU,而不至于让CPU一直空闲。 - 利用CPU的多核并行计算能力
现在的CPU基本上都是多核的。使用多线程,可以利用多核同时执行多个线程,而不至于单线程时一个核心满载,而其他核心空闲。
1.2 多线程弊端
- 线程切换是有开销的,这会导致程序运行变慢。
- 多线程程序必须非常小心地同步代码,否则会引起死锁。
- 多线程程序极难调试,并且一些bug非常隐蔽,可能你99次运行都是对的,但是有1次是错的。不像单线程程序那么容易暴露问题。
1.3 代码测试
分别创建 1,10,100,1000,10000 个线程一共做100000000循环
public class CounterDemo {
private static final long num = 1000000000L;
// 执行循环,即每个线程的任务
public static void splitCount(int threadNum) {
// 通过num/threadNum计算每个线程要循环的次数,
// 若一个线程则1000000000次,若 100000个线程则1000次
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); // 1个线程
countWithMultithread(10); // 10个线程
countWithMultithread(100); // 100个线程
countWithMultithread(1000); // 1000个线程
countWithMultithread(10000); // 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++) {
// 创建threadNum个线程
Thread thread1 = new Thread(splitCount); // 传入线程执行循环次数
list.add(thread1);
}
startTime = System.currentTimeMillis(); // 开始时间
for (Thread th: list) {
th.start();
}
for (Thread th: list) {
// 保证所有线程都在main之前运行完
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 // 10000个线程反而比1个线程执行时间长
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+次:线程切换时需要保存当前线程的数据和函数栈
- 当前任务的时间片用完之后,系统CPU正常调度下一个任务;
- 当前任务碰到IO阻塞,调度线程将挂起此任务,继续下一个任务;
- 多个任务抢占锁资源,当前任务没有抢到,被调度器挂起,继续下一个任务;
- 用户代码挂起当前任务,让出CPU时间;
- 硬件中断;
2.阻塞不占用cpu时间
2.1 CPU初始状态
2.2 线程运行
public static void main(String[] args) {
for (int i = 0; i < 10000000; i++) {
System.out.println("dfdf");
System.out.println(456 + 567);
}
}
可以看出cpu的利用率飙升到了65%,这时候cpu占用率已经很高了。
2.3 进入TIMED_WAITING 状态
public static void main(String[] args) {
for (int i = 0; i < 10000000; i++) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
下图可见cpu的利用率还是在7%左右,由此可以证明sleep方法并不会占用CPU的时间
2.4 进入WAITING 状态
public static void main(String[] args) {
for (int i = 0; i < 10000000; i++) {
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
下图可见 CPU 利用率是 6% ,由此可以证明wait方法也不会占用CPU的时间