線程安全與鎖優(yōu)化

一肆饶、線程安全的實現(xiàn)方法

(一)互斥同步

  • 互斥是實現(xiàn)同步的一種手段,臨界區(qū)(Critical Section)岖常、互斥量(Mutex)驯镊、信號量(Semaphore)都是主要的互斥實現(xiàn)方式。

    互斥量和信號量在系統(tǒng)中的任何進程都是可見的,臨界區(qū)的作用范圍僅限于本進程阿宅。

  • java中,最基本的互斥同步手段就是synchronized關(guān)鍵字笼蛛,該關(guān)鍵字經(jīng)過編譯之后洒放,會在同步塊的前后分別形成monitorentermonitorexit這兩個字節(jié)碼指令,這兩個字節(jié)碼都需要一個reference類型的參數(shù)來指明要鎖定和解鎖的對象滨砍。
  • 根據(jù)虛擬機規(guī)范的要求往湿,在執(zhí)行monitorenter指令時狭魂,首先要嘗試獲取對象的鎖榆浓。如果這個對象沒被鎖定漏益,或者當(dāng)前線程已經(jīng)擁有了那個對象的鎖砂心,則把鎖的計數(shù)器加1叠穆,相應(yīng)的在執(zhí)行monitorexit指令時將鎖計數(shù)器減1此疹,當(dāng)計數(shù)器為0時末誓,鎖就被釋放拷恨。如果獲取對象鎖失敗舔亭,那當(dāng)前線程就要阻塞等待些膨,直到對象鎖被另一個線程釋放位置。
    package tystudy.javabasic.jvm;
    
    public class MonitorTest {
        public static void main(String[] args) {
            final Object lock = new Object();
            synchronized(lock) {
                System.out.println("hello");
            }
        }
    }
    
    E:\myworkspace\my-study\common-project\java-basic\target\classes>javap -c tystudy.javabasic.jvm.MonitorTest
    Compiled from "MonitorTest.java"
    public class tystudy.javabasic.jvm.MonitorTest {
      public tystudy.javabasic.jvm.MonitorTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class java/lang/Object
           3: dup
           4: invokespecial #1                  // Method java/lang/Object."<init>":()V
           7: astore_1
           8: aload_1
           9: dup
          10: astore_2
          11: monitorenter
          12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
          15: ldc           #4                  // String hello
          17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          20: aload_2
          21: monitorexit
          22: goto          30
          25: astore_3
          26: aload_2
          27: monitorexit
          28: aload_3
          29: athrow
          30: return
        Exception table:
           from    to  target type
              12    22    25   any
              25    28    25   any
    }
    
  • java的線程是映射到操作系統(tǒng)的原生線程之上的钦铺,如果要阻塞或喚醒一個線程订雾,都需要操作系統(tǒng)來幫忙完成,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)(內(nèi)核態(tài))中矛洞,因此狀態(tài)轉(zhuǎn)換需要耗費很多的處理器時間洼哎。
    • 內(nèi)核態(tài):控制計算機的硬件資源,并提供上層應(yīng)用程序的運行環(huán)境沼本。
    • 用戶態(tài):上層應(yīng)用程序的活動空間噩峦,應(yīng)用程序的執(zhí)行必須依托于內(nèi)核態(tài)提供的資源。
    • 系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源抽兆,內(nèi)核為上層應(yīng)用提供訪問的接口壕探。

    用戶態(tài)與內(nèi)核態(tài)

  • 對于代碼簡單的同步塊,狀態(tài)轉(zhuǎn)換消耗的時間可能比用戶代碼執(zhí)行的時間還長郊丛。所以synchronized是java語言中一個重量級的操作李请。虛擬機本身也會進行一些優(yōu)化,比如在通知操作系統(tǒng)阻塞線程之前加入一段自旋等待過程厉熟,避免頻繁地切入到核心態(tài)之中导盅。
  • 除了synchronized還可以使用ReentrantLock實現(xiàn)同步,它是表現(xiàn)為api層面的互斥鎖揍瑟。相比synchronized增加了幾個高級功能:等待可中斷白翻、公平鎖、綁定條件。jdk1.6之前ReentrantLock性能更優(yōu)滤馍,jdk1.6后對synchronized進行了優(yōu)化岛琼,性能與ReentrantLock差不多。

(二)非阻塞同步

  • 隨著硬件指令集的發(fā)展巢株,我們有了另外一個選擇:基于沖突檢測的樂觀并發(fā)策略槐瑞。通俗的說,就是先進行操作阁苞,如果沒有其他線程爭用共享數(shù)據(jù)困檩,那操作就成功了,如果共享數(shù)據(jù)有爭用那槽,產(chǎn)生了沖突悼沿,那就再采取其他的補償措施(最常見的補償措施就是不斷重試,直到成功為止)骚灸,這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要把線程掛起糟趾,因此這種同步操作被稱為非阻塞同步(non-blocking synchronization)。

  • 硬件保證一個從語義看起來需要多次操作的行為通過一條處理器指令就能完成甚牲,這類指令常用的有:

    • 測試并設(shè)置(test-and-set)
    • 獲取并增加(fetch-and-increment)
    • 交換(swap)
    • 比較并交換(compare-and-swap拉讯,cas)
    • 加載鏈接/條件存儲(load-linked/store-conditional,ll/sc)
  • cas指令需要有3個操作數(shù)鳖藕,分別是內(nèi)存位置(V)魔慷、預(yù)期值(A)和新值(B)。cas指令執(zhí)行時著恩,當(dāng)且僅當(dāng)V符合舊預(yù)期值A(chǔ)時院尔,處理器用新值B更新V的值,否則它就不執(zhí)行更新喉誊,但是無論是否更新了V的值邀摆,都會返回V的舊值,上述處理過程是一個原子操作伍茄。

  • jdk1.5之后栋盹,java程序中才可以使用cas操作,該操作由sun.misc.Unsafe類里面的compareAndSwapInt()compareAndSwapLong()等幾個方法包裝提供敷矫,虛擬機在內(nèi)部對這些方法做了特殊處理例获,即時編譯出來的結(jié)果就是一條平臺相關(guān)的處理器cas指令,沒有方法調(diào)用的過程曹仗,或者可以認(rèn)為是無條件內(nèi)聯(lián)進去了榨汤。

  • cas存在ABA問題,juc為了解決這個問題提供了一個帶有標(biāo)記的原子引用類AtomicStampedReference怎茫,它可以通過控制變量值的版本來保證cas的正確性收壕,如果要解決ABA問題,改用傳統(tǒng)的互斥同步可能會比原子類更高效。

二蜜宪、鎖優(yōu)化

(一)自旋鎖與自適應(yīng)自旋

  • 掛起和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)完成虫埂,這些操作給系統(tǒng)的并發(fā)性帶來很大的壓力。所以可以讓后面請求鎖的那個線程忙循環(huán)(自旋)等待圃验,每次自旋一次就查看持有鎖的線程是否已經(jīng)釋放鎖掉伏。而不需要進入內(nèi)核態(tài)掛起線程。
  • 自旋鎖在jdk1.4.2中就已經(jīng)引入损谦,不過默認(rèn)是關(guān)閉的岖免,可以通過-XX:+UseSpinning參數(shù)來開啟岳颇,==jdk1.6中默認(rèn)開啟==照捡。
  • 自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的话侧,因此栗精,如果鎖被占用的時間很短,自旋等待的效果就會非常好瞻鹏,反之悲立,如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源新博,而不會做任何有用的工作薪夕,反而會帶來性能上的浪費。所以自旋超過指定次數(shù)仍然沒有獲取鎖應(yīng)該使用傳統(tǒng)方式掛起線程赫悄。==自旋次數(shù)的默認(rèn)值是10次原献,用戶可以使用參數(shù)-XX:PreBlockSpin來更改。==
  • jdk1.6中引入了自適應(yīng)的自旋鎖,即對于經(jīng)常很快就可以獲取鎖的情況會多自旋一會,對于很少能夠通過自旋獲取鎖的就盡早或直接進入內(nèi)核態(tài)掛起線程埂淮。

(二)鎖消除

  • 虛擬機即時編譯器在運行時會對一些代碼上要求同步姑隅,但是會對被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。鎖消除主要判斷依據(jù)來源于逃逸分析的數(shù)據(jù)支持倔撞。如果判斷在一段代碼中讲仰,堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到,那么就可以把它當(dāng)做棧上數(shù)據(jù)來對待痪蝇,認(rèn)為它們是線程私有的鄙陡,同步加鎖自然就無須進行。

  • 我們也知道躏啰,對于String是一個不可變類柔吼,對字符串的連接操作總是通過生成新的String對象來進行的,因此Javac編譯器會對String連接做自動優(yōu)化丙唧。在jdk1.5之前愈魏,會轉(zhuǎn)化為StringBuffer對象的連續(xù)append操作,在jdk1.5之后會轉(zhuǎn)化為StringBuilder對象的連續(xù)append。對于StringBuffer的連續(xù)append培漏,這個方法是同步的溪厘,鎖就是this即StringBuffer對象。虛擬機會觀察這個鎖牌柄,發(fā)現(xiàn)它的攻臺作用域被限制在concatString方法內(nèi)部畸悬。也就是說,鎖對象的所有引用永遠(yuǎn)不會“逃逸”到concatString方法之外珊佣,其他線程無法訪問到它蹋宦,因此,雖然這里有鎖咒锻,但是可以被安全地消除掉冷冗,在即時編譯后,這段代碼就會忽略掉所有的同步而直接執(zhí)行了惑艇。

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

(三)鎖粗化

  • 原則上蒿辙,我們編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小滨巴。但是對于一系列的連續(xù)操作都是對同一對象反復(fù)加鎖和解鎖思灌,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競爭恭取,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能消耗泰偿。
  • StringBuffer的連續(xù)append方法就屬于這類情況。如果虛擬機探測到有這樣一串零碎的操作都對同一對象加鎖蜈垮,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部耗跛。

(四)輕量級鎖

  • 輕量級鎖是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的。
  • 對象頭分為三個部分:
    • mark word:hashcode窃款、gc分代年齡等信息课兄、指向鎖記錄的指針、指向重量級鎖的指針晨继、偏向線程id烟阐、偏向時間戳等
    • 指向方法區(qū)對象類型數(shù)據(jù)的指針
    • 如果是數(shù)組,這里會存儲數(shù)組長度
  • 輕量級鎖能提升程序同步性能的依據(jù)是“對于絕大部分的鎖紊扬,在整個同步周期內(nèi)都是不存在競爭的”蜒茄,這是一個經(jīng)驗數(shù)據(jù)。如果沒有競爭餐屎,輕量級鎖使用cas操作避免了使用互斥量的開銷檀葛,如果存在鎖競爭,除了互斥量的開銷外腹缩,還額外發(fā)生了cas操作屿聋,因此在有競爭的情況下空扎,輕量級鎖會比傳統(tǒng)的重量級鎖更慢。
1润讥、輕量級鎖的加鎖過程:

在代碼進入同步塊的時候转锈,如果此同步對象沒有被鎖定(鎖標(biāo)識為01狀態(tài)),虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間楚殿,用于存儲鎖對象目前的Mark Word的拷貝(官方把這個拷貝叫Displaced Mark Word)撮慨。然后虛擬機將使用cas操作嘗試將對象的mark word更新為指向Lock Record的指針。

如果這個更新動作成功了脆粥,那么這個線程就擁有了該對象的鎖砌溺,并且對象Mark Word的鎖標(biāo)志位將轉(zhuǎn)變?yōu)?code>00,即表示此對象處于輕量級鎖定狀態(tài)变隔。

如果這個更新動作失敗了规伐,虛擬機首先會檢查對象Mark Word是否指向當(dāng)前線程的棧幀,如果只說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖弟胀,那就可以直接進入同步塊繼續(xù)執(zhí)行楷力,否則說明這個鎖對象已經(jīng)被其他線程搶占了喊式。

如果有兩條以上的線程爭用同一個鎖孵户,那輕量級鎖就不再有效,要膨脹為重量級鎖岔留,鎖標(biāo)志位的狀態(tài)變?yōu)?code>10夏哭,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)献联。

2竖配、輕量級鎖的解鎖過程

解鎖過程也是通過cas操作來進行的,如果對象的Mark Word仍然指向線程的鎖記錄里逆,那就用cas操作把對象當(dāng)前的Mark Word和線程中復(fù)制的Displaced Mark Word替換回來进胯,如果替換成功了,整個同步過程就完成了原押。如果替換失敗胁镐,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時诸衔,喚醒被掛起的線程盯漂。

(五)偏向鎖

  • 偏向鎖也是jdk1.6中引入的一項鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語笨农,進一步提高程序的運行性能就缆。如果說輕量級鎖是在無競爭的情況下使用cas操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉谒亦,連cas操作都不做了竭宰。
1空郊、偏向鎖原理

假設(shè)當(dāng)前虛擬機啟用了偏向鎖(==-XX:+UseBiasedLocking,這是jdk1.6的默認(rèn)值==),那么當(dāng)鎖對象第一次被線程獲取的時候切揭,虛擬機將會把對象頭中的標(biāo)志位設(shè)為01渣淳,即偏向模式。同時使用cas操作把獲取到這個鎖的線程的id記錄在對象Mark Word之中伴箩,如果cas操作成功入愧,持有偏向鎖的線程以后每次進入這個鎖相關(guān)的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking嗤谚、Unlocking及對Mark WordUpdate等)棺蛛。

當(dāng)有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結(jié)束巩步。根據(jù)鎖對象目前是否處于被鎖定的狀態(tài)旁赊,撤銷偏向后恢復(fù)到未鎖定(標(biāo)志位為01)或輕量級鎖(標(biāo)志位為00)的狀態(tài)。后續(xù)的同步操作就如上面介紹的輕量級鎖那樣執(zhí)行椅野。
如果程序中大多數(shù)的鎖總是被多個不同的線程訪問终畅,那偏向模式就是多余的,使用-XX:-UseBiasedLocking來禁止偏向鎖優(yōu)化反而可以提升性能竟闪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末离福,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子炼蛤,更是在濱河造成了極大的恐慌妖爷,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件理朋,死亡現(xiàn)場離奇詭異絮识,居然都是意外死亡,警方通過查閱死者的電腦和手機嗽上,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門次舌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人兽愤,你說我怎么就攤上這事彼念。” “怎么了烹看?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵国拇,是天一觀的道長。 經(jīng)常有香客問我惯殊,道長酱吝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任土思,我火速辦了婚禮务热,結(jié)果婚禮上忆嗜,老公的妹妹穿的比我還像新娘。我一直安慰自己崎岂,他們只是感情好捆毫,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著冲甘,像睡著了一般绩卤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上江醇,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天濒憋,我揣著相機與錄音,去河邊找鬼陶夜。 笑死凛驮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的条辟。 我是一名探鬼主播黔夭,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼羽嫡!你這毒婦竟也來了本姥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤厂僧,失蹤者是張志新(化名)和其女友劉穎扣草,沒想到半個月后了牛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颜屠,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年鹰祸,在試婚紗的時候發(fā)現(xiàn)自己被綠了甫窟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛙婴,死狀恐怖粗井,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情街图,我是刑警寧澤浇衬,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站餐济,受9級特大地震影響耘擂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜絮姆,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一醉冤、第九天 我趴在偏房一處隱蔽的房頂上張望秩霍。 院中可真熱鬧,春花似錦蚁阳、人聲如沸铃绒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颠悬。三九已至,卻和暖如春定血,著一層夾襖步出監(jiān)牢的瞬間椿疗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工糠悼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留届榄,地道東北人。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓倔喂,卻偏偏與公主長得像铝条,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子席噩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

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