揭祕上下文切換

什麼是上下文切換?

其實在單個處理器的時期,操做系統就能處理多線程併發任務。處理器給每一個線程分配 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 時期的文件讀寫操做、圖像處理以及大數據分析等。

相關文章
相關標籤/搜索