大家好,我是小水珠。
记得我刚入职上家公司的时候,恰好赶上了一次抢购活动。这是系统重构上线后经历的第一次高并发考验,如期出现了大量超时预警,不过比我预料的要好一点,起码没有挂掉重启。
通过工具分析,我发现cs(上线文切换每秒次数)指标已经接近了60w,平时的话最高5w。再通过日志分析,我发现了大量带有wait()的Exception,由此初步怀疑是大量线程处理不及时导致的,进一步锁定问题是连接池大小设置不合理。后来我就模拟了生产环境配置,对连接数压测进行调节,降低最大线程数,最后系统性能就上去了。
一 初识上下文切换
其实在单个处理器的时期,操作系统就能处理多线程并发任务。处理器给每个线程分配CPU时间片,线程在分配获得的时间片内执行任务。
CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒。在这么短的时间内线程互相切换,我们根本感觉不到,所以看上去就好像是同时进行的一样。
二 多线程上下文切换诱因
在操作系统中,上下文切换的类型还可以分为进程间的上下文切换和线程间的上下文切换。而在多线程编程中,我们主要面对的就是线程间的上下文切换导致的性能问题。下面我们就重点看看究竟是什么原因导致了多线程的上下文切换。开始之前,先看下系统线程的生命周期状态。
综合图示可知,线程主要有“新建”(NEW),“就绪”(RUNNABLE),“运行”(RUNNING),“阻塞”(BLOCKED),“死亡”(DEAD)五种状态。到了Java层面他们都被映射为了NEW,RUNABLE,BLOKED,WAITING,TEMED_WAITING,TERMINADTED6种状态。
三 发现上下文切换
我们总说上线文切换会带来系统开销,那它带来的性能问题是不是真的这么糟糕呢?我们又该怎么检测到上下文切换?上下文切换到底开销在哪些环节?接下来我将给出一些代码,来对比串联执行和并发执行的速度,然后一一解答这些问题。
执行之后,看一下两者的时间测试结果:
通过数据对比我们可以看到:串联的执行速度比并发的执行速度要快。这就是因为线程的上下文切换导致了额外的开销,使用Sychronized锁关键字,并发的执行速度也无法超越串行的执行速度,这是因为多线程同样存在着上下文切换。Redis,NodeJS的设计就很好的体现了单线程串行的优势。
在Linux系统下,可以使用Linux内核提供的vmstat命令,来监视Java程序运行过程中系统上下文切换的频率,cs如下图所示:
如果是监视某个应用的上下文切换,就可以使用pidstat命令监控制定进程的Context Switch上下文切换。
至于系统开销具体发生在切换过程中的哪些具体环节,总结如下:
- 操作系统保存和恢复上下文;
- 调度器进行线程调度;
- 处理器高速缓存重新加载;
- 上下文切换也可能导致整个高速缓存区域被冲刷,从而带来时间开销。
四 总结
上下文切换就是一个工作的线程被另一个线程暂停,另外一个线程占用了处理器开始执行任务的过程。系统和Java程序自发性以及非自发性的调用操作,就会导致上下文切换,从而带来系统开销。
线程越多,系统的运行速度不一定越快。那么我们平时在并发量比较大的情况下,什么时候用单线程,什么时候用多线程呢?
一般在单个逻辑比较简单,而且速度相对来说非常快的情况下,我们可以使用单线程。例如,我们前面讲到的Redis,从内存中读取数值,不用考虑I/O瓶颈带来的阻塞问题。而在逻辑相对来说很复杂的场景,等待时间相对较长又或者是需要大量计算的场景,我建议使用多线程来提高系统的整体性能。例如,NIO时期的文件读写操作,图像处理以及大数据分析等。