如何優(yōu)化多線程上下文切換秧了?

如果是單個線程跨扮,在CPU 調(diào)用之后,那么它基本上是不會被調(diào)度出去的验毡。如果可運(yùn)行的線程數(shù)遠(yuǎn)大于 CPU 數(shù)量衡创,那么操作系統(tǒng)最終會將某個正在運(yùn)行的線程調(diào)度出來,從而使其它線程能夠使用 CPU

晶通,這就會導(dǎo)致上下文切換璃氢。

在多線程中如果使用了競爭鎖,當(dāng)線程由于等待競爭鎖而被阻塞時狮辽,JVM 通常會將這個鎖掛起一也,并允許它被交換出去。如果頻繁地發(fā)生阻塞喉脖,CPU 密集型的程序就會發(fā)生更多的上下文切換椰苟。

在某些場景下使用多線程是非常必要的,但多線程編程給系統(tǒng)帶來了上下文切換树叽,從而增加的性能開銷也是實(shí)打?qū)嵈嬖诘挠吆D敲次覀冊撊绾蝺?yōu)化多線程上下文切換呢?

競爭鎖優(yōu)化

多線程對鎖資源的競爭會引起上下文切換题诵,還有鎖競爭導(dǎo)致的線程阻塞越多洁仗,上下文切換就越頻繁,系統(tǒng)的性能開銷也就越大性锭。由此可見赠潦,在多線程編程中,鎖其實(shí)不是性能開銷的根源篷店,競爭鎖才是祭椰。

1. 減少鎖的持有時間

鎖的持有時間越長,就意味著有越多的線程在等待該競爭資源釋放疲陕。如果是Synchronized 同步鎖資源方淤,就不僅是帶來線程間的上下文切換,還有可能會增加進(jìn)程間的上下文切換蹄殃。

可以將一些與鎖無關(guān)的代碼移出同步代碼塊携茂,尤其是那些開銷較大的操作以及可能被阻塞的操作。

2. 降低鎖的粒度

同步鎖可以保證對象的原子性诅岩,我們可以考慮將鎖粒度拆分得更小一些讳苦,以此避免所有線程對一個鎖資源的競爭過于激烈带膜。具體方式有以下兩種:

鎖分離

與傳統(tǒng)鎖不同的是,讀寫鎖實(shí)現(xiàn)了鎖分離鸳谜,也就是說讀寫鎖是由“讀鎖”和“寫鎖”兩個鎖實(shí)現(xiàn)的膝藕,其規(guī)則是可以共享讀,但只有一個寫咐扭。

這樣做的好處是芭挽,在多線程讀的時候,讀讀是不互斥的蝗肪,讀寫是互斥的袜爪,寫寫是互斥的。而傳統(tǒng)的獨(dú)占鎖在沒有區(qū)分讀寫鎖的時候薛闪,讀寫操作一般是:讀讀互斥辛馆、讀寫互斥、寫寫互斥豁延。所以在讀遠(yuǎn)大于寫的多線程場景中昙篙,鎖分離避免了在高并發(fā)讀情況下的資源競爭,從而避免了上下文切換术浪。

鎖分段

我們在使用鎖來保證集合或者大對象原子性時瓢对,可以考慮將鎖對象進(jìn)一步分解。例如胰苏,Java1.8 之前版本的 ConcurrentHashMap 就使用了鎖分段硕蛹。

3. 非阻塞樂觀鎖替代競爭鎖

volatile 關(guān)鍵字的作用是保障可見性及有序性,volatile 的讀寫操作不會導(dǎo)致上下文切換硕并, 因此開銷比較小法焰。 但是,volatile 不能保證操作變量的原子性倔毙,因?yàn)闆]有鎖的排他性埃仪。

而 CAS 是一個原子的 if-then-act 操作,CAS 是一個無鎖算法實(shí)現(xiàn)陕赃,保障了對一個共享變量讀寫操作的一致性卵蛉。CAS 操作中有 3 個操作數(shù),內(nèi)存值 V么库、舊的預(yù)期值 A 和要修改的新值 B傻丝,當(dāng)且僅當(dāng) A 和 V 相同時,將 V 修改為 B诉儒,否則什么都不做葡缰,CAS 算法將不會導(dǎo)致上下文切換。Java 的 Atomic 包就使用了 CAS 算法來更新數(shù)據(jù),就不需要額外加鎖泛释。

在 JDK1.6 中滤愕,JVM 將 Synchronized 同步鎖分為了偏向鎖、輕量級鎖怜校、偏向鎖以及重量級鎖间影,優(yōu)化路徑也是按照以上順序進(jìn)行。JIT 編譯器在動態(tài)編譯同步塊的時候茄茁,也會通過鎖消除宇智、鎖粗化的方式來優(yōu)化該同步鎖。

wait/notify 優(yōu)化

在 Java 中胰丁,我們可以通過配合調(diào)用 Object 對象的 wait() 方法和 notify() 方法或 notifyAll() 方法來實(shí)現(xiàn)線程間的通信。

在線程中調(diào)用 wait() 方法喂分,將阻塞等待其它線程的通知(其它線程調(diào)用 notify() 方法或 notifyAll() 方法)锦庸,在線程中調(diào)用 notify() 方法或 notifyAll() 方法,將通知其它線程從wait() 方法處返回蒲祈。

wait/notify 的使用導(dǎo)致了較多的上下文切換

結(jié)合以下圖片甘萧,我們可以看到,在消費(fèi)者第一次申請到鎖之前梆掸,發(fā)現(xiàn)沒有商品消費(fèi)扬卷,此時會執(zhí)行 Object.wait() 方法,這里會導(dǎo)致線程掛起酸钦,進(jìn)入阻塞狀態(tài)怪得,這里為一次上下文切換。

當(dāng)生產(chǎn)者獲取到鎖并執(zhí)行 notifyAll() 之后卑硫,會喚醒處于阻塞狀態(tài)的消費(fèi)者線程徒恋,此時這里又發(fā)生了一次上下文切換。

被喚醒的等待線程在繼續(xù)運(yùn)行時欢伏,需要再次申請相應(yīng)對象的內(nèi)部鎖入挣,此時等待線程可能需要和其它新來的活躍線程爭用內(nèi)部鎖,這也可能會導(dǎo)致上下文切換硝拧。

如果有多個消費(fèi)者線程同時被阻塞径筏,用 notifyAll() 方法,將會喚醒所有阻塞的線程障陶。而某些商品依然沒有庫存滋恬,過早地喚醒這些沒有庫存的商品的消費(fèi)線程,可能會導(dǎo)致線程再次進(jìn)入阻塞狀態(tài)咸这,從而引起不必要的上下文切換夷恍。

優(yōu)化 wait/notify 的使用,減少上下文切換

首先,我們在多個不同消費(fèi)場景中酿雪,可以使用 Object.notify() 替代 Object.notifyAll()遏暴。 因?yàn)?Object.notify() 只會喚醒指定線程,不會過早地喚醒其它未滿足需求的阻塞線程指黎,所以可以減少相應(yīng)的上下文切換朋凉。

其次,在生產(chǎn)者執(zhí)行完 Object.notify() / notifyAll() 喚醒其它線程之后醋安,應(yīng)該盡快地釋放內(nèi)部鎖杂彭,以避免其它線程在喚醒之后長時間地持有鎖處理業(yè)務(wù)操作,這樣可以避免被喚醒的線程再次申請相應(yīng)內(nèi)部鎖的時候等待鎖的釋放吓揪。

最后亲怠,為了避免長時間等待,我們常會使用 Object.wait (long)設(shè)置等待超時時間柠辞,但線程無法區(qū)分其返回是由于等待超時還是被通知線程喚醒团秽,從而導(dǎo)致線程再次嘗試獲取鎖操作,增加了上下文切換叭首。這里我建議使用 Lock 鎖結(jié)合 Condition 接口替代 Synchronized 內(nèi)部鎖中的 wait / notify习勤,實(shí)現(xiàn)等待/通知。這樣做不僅可以解決上述的 Object.wait(long) 無法區(qū)分的問題焙格,還可以解決線程被過早喚醒的問題图毕。

Condition 接口定義的 await 方法 、signal 方法和 signalAll 方法分別相當(dāng)于Object.wait()眷唉、 Object.notify() 和 Object.notifyAll()予颤。

合理地設(shè)置線程池大小,避免創(chuàng)建過多線程

線程池的線程數(shù)量設(shè)置不宜過大冬阳,因?yàn)橐坏┚€程池的工作線程總數(shù)超過系統(tǒng)所擁有的處理器數(shù)量荣瑟,就會導(dǎo)致過多的上下文切換。

在有些創(chuàng)建線程池的方法里摩泪,線程數(shù)量設(shè)置不會直接暴露給我們笆焰。比如,用 Executors.newCachedThreadPool() 創(chuàng)建的線程池见坑,該線程池會復(fù)用其內(nèi)部空閑的線程來處理新提交的任務(wù)嚷掠,如果沒有,再創(chuàng)建新的線程(不受 MAX_VALUE 限制)荞驴,這樣的線程池如果碰到大量且耗時長的任務(wù)場景不皆,就會創(chuàng)建非常多的工作線程,從而導(dǎo)致頻繁的上下文切換熊楼。因此霹娄,這類線程池就只適合處理大量且耗時短的非阻塞任務(wù)。

使用協(xié)程實(shí)現(xiàn)非阻塞等待

協(xié)程是一種比線程更加輕量級的東西,相比于由操作系統(tǒng)內(nèi)核來管理的進(jìn)程和線程犬耻,協(xié)程則完全由程序本身所控制踩晶,也就是在用戶態(tài)執(zhí)行。協(xié)程避免了像線程切換那樣產(chǎn)生的上下文切換枕磁,在性能方面得到了很大的提升渡蜻。

減少 Java 虛擬機(jī)的垃圾回收

很多 JVM 垃圾回收器(serial 收集器、ParNew 收集器)在回收舊對象時计济,會產(chǎn)生內(nèi)存碎片茸苇,從而需要進(jìn)行內(nèi)存整理,在這個過程中就需要移動存活的對象沦寂。而移動內(nèi)存對象就意味著這些對象所在的內(nèi)存地址會發(fā)生變化学密,因此在移動對象前需要暫停線程,在移動完成后需要再次喚醒該線程传藏。因此減少 JVM 垃圾回收的頻率可以有效地減少上下文切換则果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市漩氨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遗增,老刑警劉巖叫惊,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異做修,居然都是意外死亡霍狰,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門饰及,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔗坯,“玉大人,你說我怎么就攤上這事燎含”霰簦” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵屏箍,是天一觀的道長绘梦。 經(jīng)常有香客問我,道長赴魁,這世上最難降的妖魔是什么卸奉? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮颖御,結(jié)果婚禮上榄棵,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好疹鳄,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布拧略。 她就那樣靜靜地躺著,像睡著了一般尚辑。 火紅的嫁衣襯著肌膚如雪辑鲤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天杠茬,我揣著相機(jī)與錄音月褥,去河邊找鬼。 笑死瓢喉,一個胖子當(dāng)著我的面吹牛宁赤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播栓票,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼决左,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了走贪?” 一聲冷哼從身側(cè)響起佛猛,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坠狡,沒想到半個月后继找,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逃沿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年婴渡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凯亮。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡边臼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出假消,到底是詐尸還是另有隱情柠并,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布富拗,位于F島的核電站堂鲤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏媒峡。R本人自食惡果不足惜瘟栖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谅阿。 院中可真熱鬧半哟,春花似錦酬滤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戒良,卻和暖如春体捏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背糯崎。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工几缭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沃呢。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓年栓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親薄霜。 傳聞我的和親對象是個殘疾皇子某抓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容