并发真的比串行快吗?

前言
在找到这个问题答案之前,我回想了自己所有掌握的知识点,以及我的相关经验,我发现我无法回答这个问题,当然在面试中,也有类似的问题被问到过:是不是线程池越大越好?我通常的回答是,不是,要根据系统的内存情况、系统的并发情况,综合来看,但怎么综合来看,我是不知道的。
直到昨天,随手翻开《并发编程的艺术》,才让我真正找到了这个问题的答案,当然相关问题的答案也开始变得明朗了,所以今天我们要探讨的问题,就是找到并发某些情况下慢的原因。
不知道有没有小伙伴还不知道并发和串行,这里我们简单说下,并发就是我们经常说的多线程,就有由多个线程共同去完成一个任务,同步进行,目的是为了提高效率;串行也就是单线程,就是一个线程完成一个任务,效率相对比较低。好了,我们开始正文吧!
一个示例
在找到问题的答案之前,我们要先写一段测试代码:
private static final long count = 10000L;
public static void main(String[] args) throws Exception{
    concurrentcy();;
    serial();
}
private static void concurrentcy() throws Exception {
    long start = System.currentTimeMillis();
    Thread thread = new Thread(() -> {
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
    });
    thread.start();
    int b = 0;
    for (long i = 0; i < count; i++) {
        b--;
    }
    long time = System.currentTimeMillis() - start;
    thread.join();
    System.out.println(String.format("concurrecy: %dms, b=%d", time, b));
}
private static void serial() {
    long start = System.currentTimeMillis();
    int a = 0;
    for (long i = 0; i < count; i++) {
        a += 5;
    }
    int b = 0;
    for (long i = 0; i < count; i++) {
        b --;
    }
    long time = System.currentTimeMillis() - start;
    System.out.println(String.format("serial: %dms, b=%d", time, b));
}
上面的方法分别定义了一个多线程并发的方法和一个串行运行的方法,他们的作用都是执行两次for循环,循环内是简单的业务。这里需要说明的是,thread.join()就是在此处加入线程,也就是第二次执行。
运行结果
当count为10000的时候,他们的运行结果如下:

还是让人挺意外的,串行方式竟然比并行快了100倍,这个数据可能和我的电脑有关系,二代i3的老爷机,多线程也太差了吧,作者的数据是差了1ms,我这也差距太大了。
count到十万的时候,差距变小了,差了快20倍:

count到百万的时候,差了差不多十五倍:

count到千万的时候,差了差不多五倍:

count到一亿的时候,差了不到0.5倍:

好了,这电脑该换了,多线程在各种数量级下都没有串行效率高。直接上作者的结果吧:
| 循环次数 | 串行 | 并行 | 并发比串行快多少 | 
|---|---|---|---|
| 1万 | 0 | 1 | 慢 | 
| 10万 | 4 | 3 | 差不多 | 
| 100万 | 5 | 5 | 差不多 | 
| 1000万 | 18 | 9 | 快一倍 | 
| 1亿 | 130 | 77 | 快一倍 | 
结论
根据上面这些结果来看,在数据量较小的情况下,并发效率不如串行,但是随着数据量不断增大,并发的效率就体现出来了。
数据量小的时候,并行慢的原因是上下文切换比较耗时。按照我们的代码,所有业务执行完成需要切换4次:第一次,主线程切换到第一次循环线程,第一次循环线程切换到主线程,主线程切换到第二次循环线程,第二次循环线程切换到主线程。所以数据量小的时候,大部分时间都花费在线程之间的上下文切换上了,所以比较慢,后面随着数据量增加,这种上下文切换时长相比程序执行时长就可以忽略了,所以这时候就是它发挥真正的技术的时候了。
并发慢的原因在于上下文切换,所以在使用多线程的时候,我们要尽可能减少线程之间的上下文切换,最明显的一个点就是,在使用线程池的时候,不要把线程池设置过大,过大会导致上下文切换过于频繁,从而让程序效率变低。这里提供几个命令(书里面的知识),可以让你查看系统的上下文切换数据:
vmstat # 统计上下文切换次数
lmbench3 # 统计上下文切换时长
这两个工具都是linux环境的,可以协助你排查线程池的问题。
从程序层面,可以通过如下方式,减少上下文切换:
无锁并发编程 CAS算法 使用最少线程 协程 
好了,今天就到这里吧
- END -