android 多線程 — 鎖優(yōu)化

ps: 這篇文章看資料時頭疼,寫起來時更頭疼鼻种,寫完了說實話也沒多大用反番,充其量也就是多了解了一些鎖的內(nèi)容,也許扣字眼的面試官會讓我們回憶起這段蛋疼的經(jīng)歷叉钥,但是鎖優(yōu)化的知識點非常重要恬口,他決定了一個 VM 多線程并發(fā)性能的天花板和地板,當(dāng)然這都是從事 VM 改造和開發(fā)的 coder 才會頭疼的沼侣,但是我們了解一下會讓我們心里更清楚祖能,也能加深對 VM 的理解

沒妹子鎮(zhèn)樓我覺得我寫不下去



涉及到的概念

鎖優(yōu)化這里涉及到大量沒聽過的概念,一般我們在 android 開發(fā)中用不到蛾洛,估計后臺 JVM 調(diào)優(yōu)的同學(xué)應(yīng)該會接觸养铸,最常用的應(yīng)該是 VM 的同學(xué)雁芙,了解概念,理解原理钞螟,我們在寫代碼時會通透很多兔甘,有種因為生而知之而油然而生優(yōu)越感,這種感覺欲罷不能啊

4種鎖:

2種混合型鎖:

2種特性:

鎖的性能從最優(yōu)開始:偏向鎖—>輕量鎖—>自適應(yīng)自旋鎖—>重量鎖

上述除了互質(zhì)鎖鳞滨, 基本都是在 java JDK 1.6 時為了提高并發(fā)性能而添加進來的洞焙,少部分在 JDK 1.4 就有,但是當(dāng)時需要手動設(shè)置 JVM拯啦,在 JDK 1.6 之后這些都變成了默認(rèn)設(shè)置澡匪,不用再手動設(shè)置了,JDK 1.6 開發(fā)團隊為了提高并發(fā)性能使用了 HotSpot 虛擬機褒链,花費了大量的精力去實現(xiàn)各種鎖優(yōu)化技術(shù)

經(jīng)過觀察唁情,虛擬機開發(fā)團隊注意到在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間甫匹,為了這段時間去掛起和恢復(fù)線程并不值得甸鸟,這也是后面一系列鎖優(yōu)化措施出現(xiàn)的源泉,也是根本


重量鎖(互質(zhì)鎖)

重量鎖(互質(zhì)鎖)就是我們說的對象鎖兵迅,也是 Synchronize 使用的鎖抢韭,只有持有鎖的線程才能執(zhí)行同步方法,其他競爭線程都會阻塞在同步隊列中

互斥鎖存在的問題是性能不夠好恍箭,體現(xiàn)在3方面:

上面提到的問題是 JVM 限制并發(fā)性能提高的元兇,但是奈何線程的調(diào)度不是代碼級的而是內(nèi)核級的隐圾,雖然 Thread 我們可以隨便用伍掀,但是 Thread 只是內(nèi)核暴露給我們的,線程的調(diào)度完全由內(nèi)核決定暇藏,我們控制不了蜜笤,即便我們可以 sleep,yield盐碱,join把兔,wait沪伙,notify ,但是這只是我們自己覺得我們可以控制县好,但是其實內(nèi)核多線程的調(diào)度由有更多我們看不見的地方围橡。因為是內(nèi)核實現(xiàn),這不是我們能修改的缕贡,更不是隨便能修改的翁授,所以 java 開發(fā)團隊在 VM 層面找詢方法,這造成了后面一系列鎖的出現(xiàn)晾咪,目的是更高效的調(diào)度線程收擦, 減少線程阻塞的機會,減少線程間的切換


自旋鎖 / 自適應(yīng)自旋鎖

自旋鎖是互斥鎖的進步禀酱,自旋鎖在有其他線程競爭同步代碼時,我們可以讓后面請求鎖的那個線程“稍等一會”牧嫉,但不放棄處理器的執(zhí)行時間剂跟,看看持有鎖的線程是否很快就會釋放鎖,為了讓線程等待酣藻,我們只須讓線程執(zhí)行一個忙循環(huán)(自旋)曹洽,這項技術(shù)就是所謂的自旋鎖。這種優(yōu)化思路就是基于前面說的鎖阻塞的時間總是很短的考量

自旋鎖在 JDK 1.4 中就已經(jīng)引入辽剧,只不過默認(rèn)是關(guān)閉的送淆,在 JDK 1.6 中就已經(jīng)改為默認(rèn)開啟了,但是自旋鎖也有自身的局限:

  • 自旋不能代替阻塞 - 自旋雖然避免了線程切換的開銷怕轿,但是自旋不會讓出 CPU 時間偷崩,這同樣會浪費大量的 CPU 時間,所以自旋默認(rèn)次數(shù)是10次撞羽,如果自旋超過了限定的次數(shù)阐斜,那么后面的線程就得老老實實取的取掛起阻塞了
  • 不適用于單核心系統(tǒng) - 同樣大家想想,在單核心上使用自旋有效果嗎诀紊,會一直卡住 CPU 誰都別想執(zhí)行操作谒出,性能反而會更爛

所以在 JDK 1.6 中引入了自適應(yīng)自旋鎖,自旋的時間不再固定了邻奠,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定笤喳。比如在同一個鎖對象上,自旋等待剛剛成功獲得過鎖碌宴,并且持有鎖的線程正在運行中杀狡,那么虛擬機就會認(rèn)為這次自旋也很有可能再次成功,進而它將允許自旋等待持續(xù)相對更長的時間贰镣,比如100個循環(huán)捣卤。另一方面忍抽,如果對于某個鎖,自旋很少成功獲得過董朝,那在以后要獲取這個鎖時將可能省略掉自旋過程鸠项,以避免浪費處理器資源


輕量鎖

簡單來說:在線程沒有競爭的時候,采用CAS操作子姜,避免使用互斥量的開銷祟绊,這里涉及到對象頭的概念。

輕量鎖是 JDK 1.6 之中加入的哥捕,不是用來代替重量級鎖的牧抽,基于優(yōu)化在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗

原理是在對象頭 Object Header 存儲信息的能力遥赚,官方稱它為“Mark Word”扬舒,使用 CAS 原子操作更新 Mark Word 中的內(nèi)容,看示例:


  • 虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間
  • 所有爭奪的線程都會拷貝一份鎖對象的消息頭到各自的線程棧的 lock record 中
  • 通過 CAS 操作把鎖對象的消息頭凫佛,變成指向自己線程標(biāo)識符
  • CAS 成功的線程就算獲得鎖了讲坎,執(zhí)行同步操作啦
  • CAS 爭奪鎖失敗的線程會發(fā)生自旋,自旋一定次數(shù)后還是失敗的話愧薛,會修改消息頭的狀態(tài)為重量級鎖晨炕,并且自身進入阻塞狀態(tài),等待擁有鎖的線程執(zhí)行結(jié)束毫炉。也由說法說:如果有兩條以上的線程爭用同一個鎖瓮栗,那輕量級鎖就不再有效,要膨脹為重量級鎖
  • 輕量鎖能提升程序同步性能的依據(jù)是“對于絕大部分的鎖瞄勾,在整個同步周期內(nèi)都是不存在競爭的”费奸,這是一個經(jīng)驗數(shù)據(jù)。如果沒有競爭进陡,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷(線程進入阻塞狀態(tài))货邓,但如果存在鎖競爭,除了互斥量的開銷外四濒,還額外發(fā)生了CAS操作换况,因此在有競爭的情況下,輕量級鎖會比傳統(tǒng)的重量級鎖更慢

偏向鎖

簡單來說:相對于輕量級鎖盗蟆,減少了鎖重入的開銷戈二,對于第一個獲得鎖的線程,后面的執(zhí)行如果該鎖沒有被其他線程獲取喳资,則該線程將不再進行同步(CAS操作)觉吭,簡單的可以這樣理解:獲取偏向鎖的線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭

競爭的邏輯:如果一個線程獲得了鎖仆邓,那么鎖就進入偏向模式鲜滩,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)伴鳖,當(dāng)這個線程再次請求鎖時,無需再做任何同步操作徙硅,即獲取鎖的過程榜聂,這樣就省去了大量有關(guān)鎖申請的操作,從而也就提供程序的性能

偏向鎖也是 JDK 1.6 中引入的一項鎖優(yōu)化嗓蘑,目的在于消除數(shù)據(jù)在無競爭情況下的同步原語须肆,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量桩皿,那偏向鎖就是在無競爭的情況下把整個同步都消除掉豌汇,連CAS操作都不做了。偏向鎖的“偏”泄隔,是指這個鎖會偏向于第一個獲得它的線程拒贱,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取佛嬉,則持有偏向鎖的線程將永遠不需要再進行同步逻澳,達到消除同步,消除鎖的目的巷燥。當(dāng)有另外一個線程去嘗試獲取這個鎖時赡盘,偏向模式就宣告結(jié)束号枕。輕量級鎖和偏向鎖都是在沒有競爭的情況下出現(xiàn)缰揪,一旦出現(xiàn)競爭就會升級為重量級鎖

偏向鎖可以提高帶有同步但無競爭的程序性能,它同樣是一個帶有效益權(quán)衡性質(zhì)的優(yōu)化葱淳,它并不一定總是對程序運行有利钝腺,如果程序中大多數(shù)的鎖都總是被多個不同的線程訪問,那偏向模式就是多余的

優(yōu)勢:

  • 如果不存在多線程同時競爭一把鎖的時候赞厕,減少CAS操作
  • 老線程重復(fù)使用鎖艳狐,無需任何CAS操作
  • 新線程獲取偏向鎖,但是沒有競爭皿桑,只需要在滿足條件的時候CAS偏向線程ID即可
  • 完美支持重入功能毫目,而且沒有任何CAS操作

混合型互斥鎖 / 混合型自旋鎖

因為程序員往往并不能事先知道哪種方案會更好(比如, 不知道運行環(huán)境的CPU核的數(shù)量)诲侮, 操作系統(tǒng)也不知道一段指令是不是針對單核或者多核環(huán)境下做過優(yōu)化镀虐, 所以大部分操作系統(tǒng)并不嚴(yán)格區(qū)分互斥鎖和自旋鎖。 實際上沟绪,絕大部分現(xiàn)代的操作系統(tǒng)采用的是混合型互斥鎖和混合型自旋鎖

  • 混合型互斥鎖 - 在多核系統(tǒng)上起初表現(xiàn)的像自旋鎖一樣刮便, 如果一個線程不能獲取互斥鎖, 它不會馬上被切換為休眠狀態(tài)绽慈, 因為互斥量可能很快就被解鎖恨旱, 所以這種機制會表現(xiàn)的像自旋鎖一樣辈毯。 只有在一段時間以后還不能獲取鎖, 它就會被切換為休眠狀態(tài)搜贤。 如果運行在單核/單CPU上谆沃, 這種機制將不會自旋

  • 混合型自旋鎖 - 起初表現(xiàn)的和正常自旋鎖一樣, 但是為了避免浪費大量的CPU時間入客, 會有一個折中的策略管毙。 這種機制不會把線程切換到休眠態(tài),也許會決定放棄這個線程的執(zhí)行(馬上放棄或者等一段時間)并允許其他線程運行桌硫, 這樣提高了自旋鎖被解鎖的可能性(大多數(shù)情況夭咬, 線程之間的切換操作比使線程休眠而后喚醒它要昂貴, 盡管那不是很明顯)


鎖削除

鎖削除是指虛擬機對檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行削除铆隘。鎖削除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持卓舵,如果判斷到一段代碼中,在堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到膀钠,那就可以把它們當(dāng)作棧上數(shù)據(jù)對待掏湾,認(rèn)為它們是線程私有的,同步加鎖自然就無須進行肿嘲。

對于同步會不會被競爭融击,程序員我們自己應(yīng)該是很清楚的,那么為什么還要有個鎖消除呢雳窟?有許多同步措施并不是程序員自己加入的尊浪,同步的代碼在Java程序中的普遍程度也許超過了大部分讀者的想象。我們來看看下面代碼封救,這段非常簡單的代碼僅僅是輸出三個字符串相加的結(jié)果拇涤,無論是源碼字面上還是程序語義上都沒有同步

public String concatString(String s1, String s2, String s3) {  
    return s1 + s2 + s3;  
}  

Javac 會轉(zhuǎn)化成字符串連接操作

public String concatString(String s1, String s2, String s3) {  
    StringBuffer sb = new StringBuffer();  
    sb.append(s1);  
    sb.append(s2);  
    sb.append(s3);  
    return sb.toString();  
}  

在 JDK 1.5 之前,會使用 StringBuffer 對象進行 append()操作誉结,在 JDK 1.5 以后的版本中鹅士,會使用StringBuilder,這個例子其實就很好的說明了問題惩坑,上面的偏向鎖也是為了減少鎖的使用


鎖膨脹

如果一有系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖掉盅,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競爭以舒,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能損耗趾痘,如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(膨脹)到整個操作序列的外部稀轨,只需要加鎖一次就可以

由于加鎖和解鎖的開銷很大扼脐,如果不斷的加鎖和解鎖操作都是對于同一個對象,虛擬機會把整個加鎖同步的范圍擴張到操作序列的外部,就是只加一次鎖瓦侮。


CAS 擦做

隨著硬件指令集的發(fā)展艰赞,除了互斥之外我們多了一個選擇:基于沖突檢測的樂觀并發(fā)政策,先進性操作肚吏,如果沒有其他線程共享數(shù)據(jù)方妖,則操作成功;如果共享數(shù)據(jù)有爭用罚攀,產(chǎn)生沖突党觅,那就采取其他措施(不斷重試),這種樂觀的并發(fā)政策的許多實現(xiàn)不用把線程掛起斋泄。樂觀并發(fā)政策需要操作和沖突檢測兩個步驟具有原子性杯瞻,需要底層硬件完成

x86中通過cmpxchg匯編指令來完成CAS操作。CAS(Compare-and-Swap 比較和交換)與平臺相關(guān)炫掐,它有三個操作數(shù)魁莉,內(nèi)存位置值(V),舊的預(yù)期值(A)募胃,新值(B)旗唁。CAS指令執(zhí)行時,當(dāng)V=A時痹束,處理器用B的值跟新V的值检疫,否則不執(zhí)行。上述的過程是一個原子操作祷嘶。JDK1.5引入的CAS屎媳,它在sun.misc.Unsafe類里面方法提供。里面調(diào)用了Native方法

下面給一個JUC包下面的Atomic類的部分源代碼抹蚀,執(zhí)行自增操作剿牺。用到CAS企垦,里面用到了循環(huán)一直判斷环壤。里面沒有進行加鎖處理。但是也有邏輯漏洞钞诡,在111和222如果其他線程被執(zhí)行郑现,獲得V(V運來是A),將他修改為B荧降,后來又修改會A接箫,則執(zhí)行222代碼的時候認(rèn)為V沒有改變過,這就是“ABA”問題朵诫。這個問題一般沒有什么影響

//該方法實現(xiàn)了i++的非阻塞的原子操作   
   public final int getAndIncrement() {   
         for (;;) { //循環(huán),使用CAS的經(jīng)典方式,這是實現(xiàn)non-blocking方式的代價   
            int current = get();//得到現(xiàn)在的值     111  
            int next = current + 1;//通過計算得到要賦予的新值   
            if (compareAndSet(current, next)) //關(guān)鍵點,調(diào)用CAS原子更新,  222  
                 return current;   
         }   
     }   

這段看看就成了辛友,我也沒找到資料怎么說他們比較的


最后

上面說的這些只是在向大家介紹概念,面試的時候好忽悠,能加點印象分废累,但實際上這些優(yōu)化手段都是有 VM 自省決定什么時候執(zhí)行的邓梅,一般至少做 android 開發(fā)的 coder 們不用為此頭疼,我們老老實實的用 Synchronize 就好邑滨,下面有一些經(jīng)典分析大家看看吧:

如果對選擇哪種方案感到疑惑日缨, 那就使用互斥鎖吧,并且大多數(shù)現(xiàn)代的操作系統(tǒng)都允許在獲取鎖的時候自旋一段時間(混合型互斥鎖)掖看。 只有在一定條件下使用自旋鎖才可以提高性能匣距, 事實上, 你現(xiàn)在在做的項目可能沒有一個能在通過自旋鎖提高性能哎壳。 也許你考慮使用你自己定義的”鎖對象”毅待, 它可以在內(nèi)部使用互斥鎖或者自旋鎖(例如: 在創(chuàng)建鎖對象時, 用哪種機制是可配置的)归榕, 剛開始在所有的地方都是用互斥鎖恩静, 如果你認(rèn)為在有些地方用自旋鎖確實可以提高性能, 你可以試試蹲坷, 并且比較兩種情況的結(jié)果(使用一些性能評測工具)驶乾, 但一定要在單核和多核環(huán)境上測試之后再下結(jié)論(如果你的代碼是夸平臺的, 也要盡可能在不同的平臺上測試下)循签。


參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末级乐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子县匠,更是在濱河造成了極大的恐慌风科,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乞旦,死亡現(xiàn)場離奇詭異贼穆,居然都是意外死亡,警方通過查閱死者的電腦和手機兰粉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門故痊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人玖姑,你說我怎么就攤上這事愕秫。” “怎么了焰络?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵戴甩,是天一觀的道長。 經(jīng)常有香客問我闪彼,道長甜孤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮缴川,結(jié)果婚禮上囱稽,老公的妹妹穿的比我還像新娘。我一直安慰自己二跋,他們只是感情好战惊,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扎即,像睡著了一般吞获。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谚鄙,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天各拷,我揣著相機與錄音,去河邊找鬼闷营。 笑死烤黍,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傻盟。 我是一名探鬼主播速蕊,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼娘赴!你這毒婦竟也來了规哲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤诽表,失蹤者是張志新(化名)和其女友劉穎唉锌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竿奏,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡袄简,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了泛啸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绿语。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖平痰,靈堂內(nèi)的尸體忽然破棺而出汞舱,到底是詐尸還是另有隱情伍纫,我是刑警寧澤宗雇,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站莹规,受9級特大地震影響赔蒲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一舞虱、第九天 我趴在偏房一處隱蔽的房頂上張望欢际。 院中可真熱鬧,春花似錦矾兜、人聲如沸损趋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浑槽。三九已至,卻和暖如春返帕,著一層夾襖步出監(jiān)牢的瞬間桐玻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工荆萤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镊靴,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓链韭,卻偏偏與公主長得像偏竟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子敞峭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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