揭秘上下文切换

什么是上下文切换?

其实在单个处理器的时期,操做系统就能处理多线程并发任务。处理器给每一个线程分配 CPU 时间片(Time Slice),线程在分配得到的时间片内执行任务。java

CPU 时间片是 CPU 分配给每一个线程执行的时间段,通常为几十毫秒。在这么短的时间内线程互相切换,咱们根本感受不到,因此看上去就好像是同时进行的同样。编程

时间片决定了一个线程能够连续占用处理器运行的时长。当一个线程的时间片用完了,或者因自身缘由被迫暂停运行了,这个时候,另一个线程(能够是同一个线程或者其它进程的线程)就会被操做系统选中,来占用处理器。这种一个线程被暂停剥夺使用权,另一个线程被选中开始或者继续运行的过程就叫作上下文切换(Context Switch)。多线程

具体来讲,一个线程被剥夺处理器的使用权而被暂停运行,就是“切出”;一个线程被选中占用处理器开始或者继续运行,就是“切入”。在这种切出切入的过程当中,操做系统须要保存和恢复相应的进度信息,这个进度信息就是“上下文”了。并发

上下文切换就是一个工做的线程被另一个线程暂停,另一个线程占用了处理器开始执行任务的过程。系统和 Java 程序自发性以及非自发性的调用操做,就会致使上下文切换,从而带来系统开销。ide

多线程上下文切换的缘由

开始以前,先看下系统线程的生命周期状态。
揭秘上下文切换
img性能

结合图示可知,线程主要有“新建”(NEW)、“就绪”(RUNNABLE)、“运行”(RUNNING)、“阻塞”(BLOCKED)、“死亡”(DEAD)五种状态。测试

在多线程编程中,执行调用如下方法或关键字,经常就会引起自发性的上下文切换。大数据

sleep()、wait()、yield()、join()、park()、synchronized、lockthis

上下文切换带来的性能问题

咱们总说上下文切换会带来系统开销,接下来我使用一段代码,来对比串联执行和并发执行的速度:操作系统

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) {
                              e.printStackTrace();
                       }
                 }
                 long end = System.currentTimeMillis();
                 System.out.println("多线程运行时间: " + (end - start) + "ms");
                 System.out.println("计数: " + 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("单线程运行时间: " + (end - start) + "ms");
                 System.out.println("计数: " + 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();
   }
}

执行以后,看一下二者的时间测试结果:

揭秘上下文切换1572250921281

经过数据对比咱们能够看到:串联的执行速度比并发的执行速度要快。这就是由于线程的上下文切换致使了额外的开销,通常来讲使用 Synchronized 锁关键字,致使了资源竞争,从而引发了上下文切换,但即便不使用 Synchronized 锁关键字,并发的执行速度也没法超越串联的执行速度,这是由于多线程一样存在着上下文切换。Redis、NodeJS 的设计就很好地体现了单线程串行的优点。

总结

线程越多,系统的运行速度不必定越快。那么咱们平时在并发量比较大的状况下,何时用单线程,何时用多线程呢?

通常在单个逻辑比较简单,并且速度相对来很是快的状况下,咱们可使用单线程。例如,咱们前面讲到的 Redis,从内存中快速读取值,不用考虑 I/O 瓶颈带来的阻塞问题。而在逻辑相对来讲很复杂的场景,等待时间相对较长又或者是须要大量计算的场景,我建议使用多线程来提升系统的总体性能。例如,NIO 时期的文件读写操做、图像处理以及大数据分析等。

相关文章
相关标签/搜索