在并發(fā)程序中,并不是啟動(dòng)更多的線程就能讓程序最大限度地并發(fā)執(zhí)行席怪。線程數(shù)量設(shè)置太 小应闯,會(huì)導(dǎo)致程序不能充分地利用系統(tǒng)資源;線程數(shù)量設(shè)置太大挂捻,又可能帶來(lái)資源的過(guò)度競(jìng)爭(zhēng)碉纺,導(dǎo)致上下文切換帶來(lái)額外的系統(tǒng)開銷。
初識(shí)上下文切換
其實(shí)在單個(gè)處理器的時(shí)期刻撒,操作系統(tǒng)就能處理多線程并發(fā)任務(wù)骨田。處理器給每個(gè)線程分配 CPU 時(shí)間片(Time Slice),線程在分配獲得的時(shí)間片內(nèi)執(zhí)行任務(wù)疫赎。
CPU 時(shí)間片是 CPU 分配給每個(gè)線程執(zhí)行的時(shí)間段盛撑,一般為幾十毫秒。在這么短的時(shí)間內(nèi)線程互相切換捧搞,根本感覺不到抵卫,所以看上去就好像是同時(shí)進(jìn)行的一樣。
時(shí)間片決定了一個(gè)線程可以連續(xù)占用處理器運(yùn)行的時(shí)長(zhǎng)胎撇。當(dāng)一個(gè)線程的時(shí)間片用完了介粘,或者因自身原因被迫暫停運(yùn)行了,這個(gè)時(shí)候晚树,另外一個(gè)線程(可以是同一個(gè)線程或者其它進(jìn)程的線程)就會(huì)被操作系統(tǒng)選中姻采,來(lái)占用處理器。這種一個(gè)線程被暫停剝奪使用權(quán)爵憎,另外一個(gè)線程被選中開始或者繼續(xù)運(yùn)行的過(guò)程就叫做上下文切換(Context Switch)慨亲。
具體來(lái)說(shuō),一個(gè)線程被剝奪處理器的使用權(quán)而被暫停運(yùn)行宝鼓,就是“切出”刑棵;一個(gè)線程被選中占用處理器開始或者繼續(xù)運(yùn)行,就是“切入”愚铡。在這種切出切入的過(guò)程中蛉签,操作系統(tǒng)需要保存和恢復(fù)相應(yīng)的進(jìn)度信息胡陪,這個(gè)進(jìn)度信息就是“上下文”了。
那上下文都包括哪些內(nèi)容呢碍舍?具體來(lái)說(shuō)柠座,它包括了寄存器的存儲(chǔ)內(nèi)容以及程序計(jì)數(shù)器存儲(chǔ)的指令內(nèi)容。CPU 寄存器負(fù)責(zé)存儲(chǔ)已經(jīng)片橡、正在和將要執(zhí)行的任務(wù)妈经,程序計(jì)數(shù)器負(fù)責(zé)存儲(chǔ)CPU 正在執(zhí)行的指令位置以及即將執(zhí)行的下一條指令的位置。
在當(dāng)前 CPU 數(shù)量遠(yuǎn)遠(yuǎn)不止一個(gè)的情況下捧书,操作系統(tǒng)將 CPU輪流分配給線程任務(wù)狂塘,此時(shí)的上下文切換就變得更加頻繁了,并且存在跨 CPU 上下文切換鳄厌,比起單核上下文切換,跨核切換更加昂貴妈踊。
多線程上下文切換誘因
在操作系統(tǒng)中了嚎,上下文切換的類型還可以分為進(jìn)程間的上下文切換和線程間的上下文切換。而在多線程編程中廊营,我們主要面對(duì)的就是線程間的上下文切換導(dǎo)致的性能問(wèn)題歪泳,下面我們就重點(diǎn)看看究竟是什么原因?qū)е铝硕嗑€程的上下文切換。開始之前露筒,先看下 Java 線程的生命周期狀態(tài)呐伞。
線程主要有“新建”(NEW)、“就緒”(RUNNABLE)慎式、“運(yùn)行”(RUNNING)伶氢、“阻塞”(BLOCKED)、“死亡”(DEAD)五種狀態(tài)瘪吏。
在這個(gè)運(yùn)行過(guò)程中癣防,線程由 RUNNABLE 轉(zhuǎn)為非RUNNABLE 的過(guò)程就是線程上下文切換。一個(gè)線程的狀態(tài)由 RUNNING 轉(zhuǎn)為 BLOCKED 掌眠,再由BLOCKED 轉(zhuǎn)為 RUNNABLE 蕾盯,然后再被調(diào)度器選中執(zhí)行,這就是一個(gè)上下文切換的過(guò)程蓝丙。
當(dāng)一個(gè)線程從 RUNNING 狀態(tài)轉(zhuǎn)為 BLOCKED 狀態(tài)時(shí)级遭,我們稱為一個(gè)線程的暫停,線程暫停被切出之后渺尘,操作系統(tǒng)會(huì)保存相應(yīng)的上下文挫鸽,以便這個(gè)線程稍后再次進(jìn)入RUNNABLE 狀態(tài)時(shí)能夠在之前執(zhí)行進(jìn)度的基礎(chǔ)上繼續(xù)執(zhí)行。
當(dāng)一個(gè)線程從 BLOCKED 狀態(tài)進(jìn)入到 RUNNABLE 狀態(tài)時(shí)沧烈,我們稱為一個(gè)線程的喚醒掠兄,此時(shí)線程將獲取上次保存的上下文繼續(xù)完成執(zhí)行。
通過(guò)線程的運(yùn)行狀態(tài)以及狀態(tài)間的相互切換,我們可以了解到蚂夕,多線程的上下文切換實(shí)際上就是由多線程兩個(gè)運(yùn)行狀態(tài)的互相切換導(dǎo)致的迅诬。
那么在線程運(yùn)行時(shí),線程狀態(tài)由 RUNNING 轉(zhuǎn)為BLOCKED 或者由 BLOCKED 轉(zhuǎn)為 RUNNABLE婿牍,這又是什么誘發(fā)的呢侈贷?
我們可以分兩種情況來(lái)分析,一種是程序本身觸發(fā)的切換等脂,這種我們稱為自發(fā)性上下文切換俏蛮,另一種是由系統(tǒng)或者虛擬機(jī)誘發(fā)的非自發(fā)性上下文切換。 自發(fā)性上下文切換指線程由 Java 程序調(diào)用導(dǎo)致切出上遥,在多線程編程中搏屑,執(zhí)行調(diào)用以下方法或關(guān)鍵字,常常就會(huì)引發(fā)自發(fā)性上下文切換粉楚。
sleep()
wait()
yield()
join()
park()
synchronized
lock
非自發(fā)性上下文切換指線程由于調(diào)度器的原因被迫切出辣恋。常見的有:線程被分配的時(shí)間片用完,虛擬機(jī)垃圾回收導(dǎo)致或者執(zhí)行優(yōu)先級(jí)的問(wèn)題導(dǎo)致模软。
“虛擬機(jī)垃圾回收為什么會(huì)導(dǎo)致上下文切換”伟骨。在 Java 虛擬機(jī)中,對(duì)象的內(nèi)存都是由虛擬機(jī)中的堆分配的燃异,在程序運(yùn)行過(guò)程中携狭,新的對(duì)象將不斷被創(chuàng)建,如果舊的對(duì)象使用后不進(jìn)行回收回俐,堆內(nèi)存將很快被耗盡逛腿。Java虛擬機(jī)提供了一種回收機(jī)制,對(duì)創(chuàng)建后不再使用的對(duì)象進(jìn)行回收仅颇,從而保證堆內(nèi)存的可持續(xù)性分配鳄逾。而這種垃圾回收機(jī)制的使用有可能會(huì)導(dǎo)致 stop-the-world 事件的發(fā)生,這其實(shí)就是一種線程暫停行為灵莲。
發(fā)現(xiàn)上下文切換
我們總說(shuō)上下文切換會(huì)帶來(lái)系統(tǒng)開銷雕凹,那它帶來(lái)的性能問(wèn)題是不是真有這么糟糕呢?我們又該怎么去監(jiān)測(cè)到上下文切換政冻?上下文切換到底開銷在哪些環(huán)節(jié)枚抵?
串聯(lián)的執(zhí)行速度比并發(fā)的執(zhí)行速度要快。這就是因?yàn)榫€程的上下文切換導(dǎo)致了額外的開銷明场,使用 Synchronized 鎖關(guān)鍵字汽摹,導(dǎo)致了資源競(jìng)爭(zhēng),從而引起了上下文切換苦锨,但即使不使用 Synchronized 鎖關(guān)鍵字逼泣,并發(fā)的執(zhí)行速度也無(wú)法超越串聯(lián)的執(zhí)行速度趴泌,這是因?yàn)槎嗑€程同樣存在著上下文切換。Redis拉庶、NodeJS 的設(shè)計(jì)就很好地體現(xiàn)了單線程串行的優(yōu)勢(shì)嗜憔。
在 Linux 系統(tǒng)下,可以使用 Linux 內(nèi)核提供的 vmstat 命令氏仗,來(lái)監(jiān)視 Java 程序運(yùn)行過(guò)程中系統(tǒng)的上下文切換頻率
如果是監(jiān)視某個(gè)應(yīng)用的上下文切換吉捶,就可以使用 pidstat 命令監(jiān)控指定進(jìn)程的 Context Switch 上下文切換。
由于 Windows 沒(méi)有像 vmstat 這樣的工具皆尔,在 Windows下呐舔,我們可以使用 Process Explorer,來(lái)查看程序執(zhí)行時(shí)慷蠕, 線程間上下文切換的次數(shù)珊拼。
至于系統(tǒng)開銷具體發(fā)生在切換過(guò)程中的哪些具體環(huán)節(jié),總結(jié)如下:
操作系統(tǒng)保存和恢復(fù)上下文流炕;
調(diào)度器進(jìn)行線程調(diào)度杆麸;
處理器高速緩存重新加載;
上下文切換也可能導(dǎo)致整個(gè)高速緩存區(qū)被沖刷浪感,從而帶來(lái)時(shí)間開銷。