線程上下文切換(Thread Context Switch )
定義
CPU執行線程的時候是通過時間分片的方式來輪流執行的,當某一個線程的時間片用完(到期),那么這個線程就會被中斷,CPU不再執行當前線程,CPU會把使用權給其它線程來執行。如T1線程未執行結束,T2/T3線程插進來執行了,若干時間后T1又繼續執行未執行完的部分,這種就造成了線程之間的來回切換。
一次上下文切換:CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片后會切換到下一個任務,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再次加載這個任務的狀態,從任務保存到再加載的過程就是一次上下文切換。當Context Switch發生時,需要由操作系統保持當前線程的狀態,並恢復另一個線程的狀態,狀態包括程序計數器、虛擬機棧中每個棧幀的信息。
造成原因
- 線程的CPU時間片用完
- 垃圾回收
- 有更高優先級的線程需要運行
- 線程自已調用了sleep、yield、wait、park、synchronized、lock等方法
並發編程的目的是為了讓程序運行得更快,但是並不是啟動更多的線程就能讓程序最大限度地並發執行。在進行並發編程時,如果希望通過多線程執行任務讓程序運行得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬件和軟件的資源限制問題,本文要研究的是上下文切換的問題。
這就像我們同時讀兩本書,當我們在讀一本英文的技術書籍時,發現某個單詞不認識, 於是便打開中英文詞典,但是在放下英文書籍之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之后,能夠繼續讀這本書。這樣的切換是會影響讀 書效率的,同樣上下文切換也會影響多線程的執行速度。
代碼演示:
public class SwitchContextTest { public static void main(String[] args) throws InterruptedException { new SwitchContextTest().contextSwitchTest(); } public static final long REPEAT_TIMES = 100000; public AtomicInteger count = new AtomicInteger(); /** * 啟動10000個線程,表示有10000個線程會來回上下文切換 */ public void contextSwitchTest() { long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { new Thread(() -> { for (int j = 0; j < REPEAT_TIMES; j++) { count.incrementAndGet(); } }, "AAA").start(); } // 主線程 + 后台gc線和,所以這里判斷條件為2 while (Thread.activeCount() > 2) { Thread.yield(); } long end = System.currentTimeMillis(); System.out.println("最終結果:" + count.get() + "共執行:" + (end - start) + "ms"); // 最終結果:1000000000共執行:20227ms } /** * 啟動1個線程,沒有線程會來回上下文切換 */ public void noSwitchtest() { long start = System.currentTimeMillis(); for (int i = 0; i < 10000 * REPEAT_TIMES; i++) { count.incrementAndGet(); } long end = System.currentTimeMillis(); System.out.println("最終結果:" + count.get() + "共執行:" + (end - start) + "ms"); // 最終結果:1000000000共執行:5613ms } }
在總的循環次數一樣時,一個使用多線程,一個使用單線程,兩個方法測試出的結果顯示使用多線程的消耗總時間比單線程下還要長,這個實例說明多線程中,線程上下文之間的切換比較耗性能。
如何減少上下文切換
既然上下文切換會導致額外的開銷,因此減少上下文切換次數便可以提高多線程程序的運行效率。減少上下文切換的方法有無鎖並發編程、CAS算法、使用最少線程和使用協程。
- 無鎖並發編程。多線程競爭時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash取模分段,不同的線程處理不同段的數據
- CAS算法。Java的Atomic包使用CAS算法來更新數據,而不需要加鎖
- 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態
- 協程。在單線程里實現多任務的調度,並在單線程里維持多個任務間的切換