双十一前的一个多月,所有的电商相关的系统都在进行压测,不断的优化系统,我们的电商ERP系统也进行了一个多月的压测和优化的过程,在这其中,我们发现了大量的超时报警,通过工具分析,我们发现是cs指标很高,然后分析日志,我们发现有大量wait()相关的Exception,这个时候我们怀疑是在多线程并发处理的时候,出现了大量的线程处理不及时导致的这些问题,后来我们通过减小线程池最大线程数,再进行压测发现系统的性能有了不小的提升。
我们都知道,在并发编程中,并不是线程越多就效率越高,线程数太少可能导致资源不能充分利用,线程数太多可能导致竞争资源激烈,然后上下文切换频繁造成系统的额外开销。
我们都知道,在处理多线程并发任务的时候,处理器会给每个线程分配CPU时间片,线程在各自分配的时间片内执行任务,每个时间片的大小一般为几十毫秒,所以在一秒钟就可能发生几十上百次的线程相互切换,给我们的感觉就是同时进行的。
线程只在分配的时间片内占用处理器,当一个线程分配的时间片用完了,或者自身原因被迫暂停运行的时候,就会有另外一个线程来占用这个处理器,这种一个线程让出处理器使用权,另外一个线程获取处理器使用权的过程就叫做上下文切换。
一个线程让出处理器使用权,就是“切出”;另外一个线程获取处理器使用权。就是“切入”,在这个切入切出的过程中,操作系统会保存和恢复相关的进度信息,这个进度信息就是我们常说的“上下文”,上下文中一般包含了寄存器的存储内容以及程序计数器存储的指令内容。
多线程编程中,我们知道线程间的上下文切换会导致性能问题,那么是什么原因造成的线程间的上下文切换。我们先看一下线程的生命周期,从中看一下找找答案。
线程的五种状态我们都非常清楚:NEW、RUNNABLE、RUNNING、BLOCKED、DEAD,对应的Java中的六种状态分别为:NEW、RUNABLE、BLOCKED、WAINTING、TIMED_WAITING、TERMINADTED。
图中,一个线程从RUNNABLE到RUNNING的过程就是线程的上下文切换,RUNNING状态到BLOCKED、再到RUNNABLE、再从RUNNABLE到RUNNING的过程就是一个上下文切换的过程。一个线程从RUNNING转为BLOCKED状态时,我们叫做线程的暂停,线程暂停了,这个处理器就会有别的线程来占用,操作系统就会保存相应的上下文,为了这个线程以后再进入RUNNABLE状态时可以接着之前的执行进度继续执行。当线程从BLOCKED状态进入到RUNNABLE时,也就是线程的唤醒,此时线程将获取上次保存的上下文信息。
我们看到,多线程的上下文切换实际上就是多线程两个运行状态的相互切换导致的。
我们知道两种情况可以导致上下文切换:一种是程序本身触发的切换,这种我们一般称为自发性上下文切换,另一种是系统或者虚拟机导致的上下文切换,我们称之为非自发性上下文切换。
自发性上下文是线程由Java程序调用导致切出,一般是在编码的时候,调用一下几个方法或关键字:
sleep()wait()yield()join()park();synchronizedlock
非自发的上下文切换常见的有:线程被分配的时间片用完,虚拟机垃圾回收导致,或者执行优先级的问题导致。
我们通过一个例子来看一下并发执行和串行执行的速度对比:
public class DemoApplication { public static void main(String[] args) { //运行多线程 MultiThreadTester test1 = new MultiThreadTester(); test1.Start(); //运行单线程 SerialTester test2 = new SerialTester(); test2.Start(); } static class MultiThreadTester extends ThreadContextSwitchTester { @Override public void Start() { long start = System.currentTimeMillis(); MyRunnable myRunnable1 = new MyRunnable(); Thread[] threads = new Thread[4]; //创建多个线程 for (int i = 0; i < 4; i++) { threads[i] = new Thread(myRunnable1); threads[i].start(); } for (int i = 0; i < 4; i++) { try { //等待一起运行完 threads[i].join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println("multi thread exce time: " + (end - start) + "s"); System.out.println("counter: " + counter); } // 创建一个实现Runnable的类 class MyRunnable implements Runnable { public void run() { while (counter < 100000000) { synchronized (this) { if(counter < 100000000) { increaseCounter(); } } } } } } //创建一个单线程 static class SerialTester extends ThreadContextSwitchTester{ @Override public void Start() { long start = System.currentTimeMillis(); for (long i = 0; i < count; i++) { increaseCounter(); } long end = System.currentTimeMillis(); System.out.println("serial exec time: " + (end - start) + "s"); System.out.println("counter: " + counter); } } //父类 static abstract class ThreadContextSwitchTester { public static final int count = 100000000; public volatile int counter = 0; public int getCount() { return this.counter; } public void increaseCounter() { this.counter += 1; } public abstract void Start(); } }
执行结果:
multi thread exce time: 5149scounter: 100000000serial exec time: 956scounter: 100000000
通过执行的结果对比我们可以看到,串行的执行速度比并发执行的速度更快,这其中就是因为多线程的上下文切换导致了系统额外的开销,使用的synchronized关键字,导致了锁竞争,导致了线程上下文切换,这个地方如果不使用synchronized关键字,并发的执行效率也比不上串行执行的速度,因为没有锁竞争多线程的上下文切换依然存在。
系统开销在上下文切换的哪些环节:
上下文就是一个释放处理器的使用权,另外一个线程获取处理器的使用权,自发和非自发的调用操作,都会导致上下文切换,会导致系统资源开销。线程越多不一定执行的速度越快,在单个逻辑比较简单的时候,而且速度相对来说非常快的情况下,我们推荐是使用单线程。如果逻辑非常复杂,或者需要进行大量的计算的地方,我们建议使用多线程来提高系统的性能。