面試官:單核 CPU 支持 Java 多線程嗎?為什么?被問懵了!
由于現在大多計算機都是多核CPU,多線程往往會比單線程更快,更能夠提高并發,但提高并發并不意味著啟動更多的線程來執行。更多的線程意味著線程創建銷毀開銷加大、上下文非常頻繁,你的程序反而不能支持更高的TPS。
時間片
多任務系統往往需要同時執行多道作業。作業數往往大于機器的CPU數,然而一顆CPU同時只能執行一項任務,如何讓用戶感覺這些任務正在同時進行呢? 操作系統的設計者 巧妙地利用了時間片輪轉的方式
時間片是CPU分配給各個任務(線程)的時間!
“思考:單核CPU為何也支持多線程呢?”
線程上下文是指某一時間點 CPU 寄存器和程序計數器的內容,CPU通過時間片分配算法來循環執行任務(線程),因為時間片非常短,所以CPU通過不停地切換線程執行。
換言之,單CPU這么頻繁,多核CPU一定程度上可以減少上下文切換。
超線程
現代CPU除了處理器核心之外還包括寄存器、L1L2緩存這些存儲設備、浮點運算單元、整數運算單元等一些輔助運算設備以及內部總線等。一個多核的CPU也就是一個CPU上有多個處理器核心,就意味著程序的不同線程需要經常在CPU之間的外部總線上通信,同時還要處理不同CPU之間不同緩存導致數據不一致的問題。
超線程這個概念是Intel提出的,簡單來說是在一個CPU上真正的并發兩個線程,由于CPU都是分時的(如果兩個線程A和B,A正在使用處理器核心,B正在使用緩存或者其他設備,那AB兩個線程就可以并發執行,但是如果AB都在訪問同一個設備,那就只能等前一個線程執行完后一個線程才能執行)。實現這種并發的原理是 在CPU里加了一個協調輔助核心,根據Intel提供的數據,這樣一個設備會使得設備面積增大5%,但是性能提高15%~30%。
上下文切換
- 線程切換,同一進程中的兩個線程之間的切換
- 進程切換,兩個進程之間的切換
- 模式切換,在給定線程中,用戶模式和內核模式的切換
- 地址空間切換,將虛擬內存切換到物理內存
CPU切換前把當前任務的狀態保存下來,以便下次切換回這個任務時可以再次加載這個任務的狀態,然后加載下一任務的狀態并執行。任務的狀態保存及再加載, 這段過程就叫做上下文切換。
每個線程都有一個程序計數器(記錄要執行的下一條指令),一組寄存器(保存當前線程的工作變量),堆棧(記錄執行歷史,其中每一幀保存了一個已經調用但未返回的過程)。
寄存器 是 CPU 內部的數量較少但是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度。
程序計數器是一個專用的寄存器,用于表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置。
- 掛起當前任務(線程/進程),將這個任務在 CPU 中的狀態(上下文)存儲于內存中的某處
- 恢復一個任務(線程/進程),在內存中檢索下一個任務的上下文并將其在 CPU 的寄存器中恢復
- 跳轉到程序計數器所指向的位置(即跳轉到任務被中斷時的代碼行),以恢復該進程在程序中]

線程上下文切換會有什么問題呢?
上下文切換會導致額外的開銷,常常表現為高并發執行時速度會慢串行,因此減少上下文切換次數便可以提高多線程程序的運行效率。
- 直接消耗:指的是CPU寄存器需要保存和加載, 系統調度器的代碼需要執行, TLB實例需要重新加載, CPU 的pipeline需要刷掉
- 間接消耗:指的是多核的cache之間得共享數據, 間接消耗對于程序的影響要看線程工作區操作數據的大小
切換查看
Linux系統下可以使用vmstat命令來查看上下文切換的次數, 其中cs列就是指上下文切換的數目(一般情況下, 空閑系統的上下文切換每秒大概在1500以下)

線程調度
搶占式調度
指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導致整個進程堵塞。
java使用的線程調使用搶占式調度,Java中線程會按優先級分配CPU時間片運行,且優先級越高越優先執行,但優先級高并不代表能獨自占用執行時間片,可能是優先級高得到越多的執行時間片,反之,優先級低的分到的執行時間少但不會分配不到執行時間。

協同式調度
指某一線程執行完后主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個致命弱點:如果一個線程編寫有問題,運行到一半就一直堵塞,那么可能導致整個系統崩潰。

線程讓出cpu的情況
- 當前運行線程主動放棄CPU,JVM暫時放棄CPU操作(基于時間片輪轉調度的JVM操作系統不會讓線程永久放棄CPU,或者說放棄本次時間片的執行權),例如調用
yield()方法。 - 當前運行線程因為某些原因進入阻塞狀態,例如阻塞在I/O上
- 當前運行線程結束,即運行完
run()方法里面的任務
引起線程上下文切換的因素
- 當前執行任務(線程)的時間片用完之后,系統CPU正常調度下一個任務
- 中斷處理,在中斷處理中,其他程序”打斷”了當前正在運行的程序。當CPU接收到中斷請求時,會在正在運行的程序和發起中斷請求的程序之間進行一次上下文切換。中斷分為硬件中斷和軟件中斷,軟件中斷包括因為IO阻塞、未搶到資源或者用戶代碼等原因,線程被掛起。
- 用戶態切換,對于一些操作系統,當進行用戶態切換時也會進行一次上下文切換,雖然這不是必須的。
- 多個任務搶占鎖資源,在多任務處理中,CPU會在不同程序之間來回切換,每個程序都有相應的處理時間片,CPU在兩個時間片的間隔中進行上下文切換
因此優化手段有:
- 無鎖并發編程,多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash取模分段,不同的線程處理不同段的數據
- CAS算法,Java的Atomic包使用CAS算法來更新數據,而不需要加鎖
- 使用最少線程
- 協程,單線程里實現多任務的調度,并在單線程里維持多個任務間的切換
合理設置線程數目既可以最大化利用CPU,又可以減少線程切換的開銷。
- 高并發,低耗時的情況,建議少線程。
- 低并發,高耗時的情況:建議多線程。
- 高并發高耗時,要分析任務類型、增加排隊、加大線程數