前言
本文來自方騰飛老師《Java并發程式設計的藝術》第一章。
并發程式設計的目的是為了讓程式運作得更快,但是并不是啟動更多的線程就能讓程式最大限度地并發執行。在進行并發程式設計時,如果希望通過多線程執行任務讓程式運作得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限于硬體和軟體的資源限制問題,本文要研究的是上下文切換的問題。
什麼是上下文切換
即使是單核CPU也支援多線程執行代碼,CPU通過給每個線程配置設定CPU時間片來實作這個機制。時間片是CPU配置設定給各個線程的時間,因為時間片非常短,是以CPU通過不停地切換線程執行,讓我們感覺多個線程時同時執行的,時間片一般是幾十毫秒(ms)。
CPU通過時間片配置設定算法來循環執行任務,目前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀态,以便下次切換回這個任務時,可以再次加載這個任務的狀态,從任務儲存到再加載的過程就是一次上下文切換。
這就像我們同時讀兩本書,當我們在讀一本英文的技術書籍時,發現某個單詞不認識,于是便打開中英文詞典,但是在放下英文書籍之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。
上下文切換代碼測試
下面的代碼示範串行和并發執行并累加操作的時間:
1 public class ContextSwitchTest
2 {
3 private static final long count = 10000;
4
5 public static void main(String[] args) throws Exception
6 {
7 concurrency();
8 serial();
9 }
10
11 private static void concurrency() throws Exception
12 {
13 long start = System.currentTimeMillis();
14 Thread thread = new Thread(new Runnable(){
15 public void run()
16 {
17 int a = 0;
18 for (int i = 0; i < count; i++)
19 {
20 a += 5;
21 }
22 }
23 });
24 thread.start();
25 int b = 0;
26 for (long i = 0; i < count; i++)
27 {
28 b --;
29 }
30 thread.join();
31 long time = System.currentTimeMillis() - start;
32 System.out.println("Concurrency:" + time + "ms, b = " + b);
33 }
34
35 private static void serial()
36 {
37 long start = System.currentTimeMillis();
38 int a = 0;
39 for (long i = 0; i < count; i++)
40 {
41 a += 5;
42 }
43 int b = 0;
44 for (int i = 0; i < count; i++)
45 {
46 b --;
47 }
48 long time = System.currentTimeMillis() - start;
49 System.out.println("Serial:" + time + "ms, b = " + b + ", a = " + a);
50 }
51 }
修改上面的count值,即修改循環次數,看一下串行運作和并發運作的時間測試結果:
循環次數 | 串行執行耗時/ms | 并發執行耗時/ms | 串行和并發對比 |
1億 | 78 | 50 | 并發快約0.5倍 |
1000萬 | 10 | 6 | 并發快約0.5~1倍 |
100萬 | 3 | 2 | 差不多 |
10萬 | |||
1萬 | 1 | 差不多,十幾次執行下來,總體而言串行略快 |
從表中可以看出,100次并發執行累加以下,串行執行和并發執行的運作速度總體而言差不多,1萬次以下串行執行甚至還可以說是略快。為什麼并發執行的速度會比串行慢呢?這就是因為線程有建立和上下文切換的開銷。
引起線程上下文切換的原因
對于我們經常使用的搶占式作業系統而言,引起線程上下文切換的原因大概有以下幾種:
- 目前執行任務的時間片用完之後,系統CPU正常排程下一個任務
- 目前執行任務碰到IO阻塞,排程器将此任務挂起,繼續下一任務
- 多個任務搶占鎖資源,目前任務沒有搶到鎖資源,被排程器挂起,繼續下一任務
- 使用者代碼挂起目前任務,讓出CPU時間
- 硬體中斷
上下文切換次數檢視
在Linux系統下可以使用vmstat指令來檢視上下文切換的次數,下面是利用vmstat檢視上下文切換次數的示例:

CS(Context Switch)表示上下文切換的次數,從圖中可以看到,上下文每秒鐘切換500~600次左右。
如果要檢視上下文切換的時長,可以利用Lmbench3,這是一個性能分析工具。
如何減少上下文切換
既然上下文切換會導緻額外的開銷,是以減少上下文切換次數便可以提高多線程程式的運作效率。減少上下文切換的方法有無鎖并發程式設計、CAS算法、使用最少線程和使用協程。
- 無鎖并發程式設計。多線程競争時,會引起上下文切換,是以多線程處理資料時,可以用一些辦法來避免使用鎖,如将資料的ID按照Hash取模分段,不同的線程處理不同段的資料
- CAS算法。Java的Atomic包使用CAS算法來更新資料,而不需要加鎖
- 使用最少線程。避免建立不需要的線程,比如任務很少,但是建立了很多線程來處理,這樣會造成大量線程都處于等待狀态
- 協程。在單線程裡實作多任務的排程,并在單線程裡維持多個任務間的切換
==================================================================================
我不能保證寫的每個地方都是對的,但是至少能保證不複制、不黏貼,保證每一句話、每一行代碼都經過了認真的推敲、仔細的斟酌。每一篇文章的背後,希望都能看到自己對于技術、對于生活的态度。
我相信喬布斯說的,隻有那些瘋狂到認為自己可以改變世界的人才能真正地改變世界。面對壓力,我可以挑燈夜戰、不眠不休;面對困難,我願意迎難而上、永不退縮。
其實我想說的是,我隻是一個程式員,這就是我現在純粹人生的全部。