Java 中的鎖 —— 自旋鎖、偏向鎖颤枪、輕量級鎖汗捡、重量級鎖

理解鎖的基本知識

1. 鎖的類型

鎖從宏觀上分類,分為悲觀鎖與樂觀鎖畏纲。

  • 樂觀鎖
    樂觀鎖是一種樂觀思想凉唐,即認(rèn)為讀多寫少,遇到并發(fā)寫的可能性低霍骄,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改台囱,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)读整,采取在寫時先讀出當(dāng)前版本號簿训,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新)米间,如果失敗則要重復(fù)讀-比較-寫的操作强品。
    java 中的樂觀鎖基本都是通過 CAS 操作實現(xiàn)的,CAS 是一種更新的原子操作屈糊,比較當(dāng)前值跟傳入值是否一樣的榛,一樣則更新,否則失敗逻锐。

  • 悲觀鎖
    悲觀鎖是就是悲觀思想夫晌,即認(rèn)為寫多雕薪,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改晓淀,所以每次在讀寫數(shù)據(jù)的時候都會上鎖所袁,這樣別人想讀寫這個數(shù)據(jù)就會 block 直到拿到鎖。java 中的悲觀鎖就是Synchronized凶掰,AQS 框架下的鎖則是先嘗試 CAS 樂觀鎖去獲取鎖燥爷,獲取不到,才會轉(zhuǎn)換為悲觀鎖懦窘,如ReentrantLock前翎。

2. Java 線程阻塞的代價

java 的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)介入畅涂,需要在用戶態(tài)與核心態(tài)之間切換港华,這種切換會消耗大量的系統(tǒng)資源,操作系統(tǒng)的這種切換操作稱之為互斥(mutex)毅戈。
JDK1.6之前,Synchronized會導(dǎo)致爭用不到鎖的線程進(jìn)入阻塞狀態(tài)愤惰,被稱為重量級鎖苇经,為了緩解上述性能問題,JVM 從1.6開始宦言,對Synchronized關(guān)鍵字做了優(yōu)化扇单,引入了輕量鎖與偏向鎖,默認(rèn)啟用了自旋鎖奠旺,他們都屬于樂觀鎖蜘澜。

3. markword

HotSpot 虛擬機的對象頭包括兩部分信息

  1. markword:用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)响疚、GC分代年齡鄙信、鎖狀態(tài)標(biāo)志、線程持有的鎖忿晕、偏向線程ID装诡、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit践盼,官方稱它為“MarkWord”鸦采。
  2. klass:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例咕幻。

markword 數(shù)據(jù)的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit渔伯,它的最后2bit是鎖狀態(tài)標(biāo)志位,用來標(biāo)記當(dāng)前對象的狀態(tài)肄程,對象所處的狀態(tài)锣吼,決定了 markword 存儲的內(nèi)容选浑,32位虛擬機在不同狀態(tài)下 markword 結(jié)構(gòu)如下圖所示:

上圖中,我們提到了 java 的4種鎖吐限,他們分別是重量級鎖鲜侥、自旋鎖、輕量級鎖和偏向鎖诸典。不同的鎖有不同的特點描函,每種鎖只有在其特定的場景下,才會有出色的表現(xiàn)狐粱,java 中沒有哪種鎖能夠在所有情況下都能有出色的效率舀寓,引入這么多鎖的原因就是為了應(yīng)對不同的場景。

前面講到了重量級鎖是悲觀鎖的一種肌蜻,自旋鎖互墓、輕量級鎖與偏向鎖屬于樂觀鎖。下面我們具體來分析下他們的特效蒋搜;

自旋鎖

自旋鎖原理非常簡單篡撵,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài)豆挽,它們只需要等一等(自旋)育谬,等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗帮哈。

但是線程自旋是需要消耗 cup 的膛檀,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖娘侍,那線程也不能一直占用 cup 自旋做無用功咖刃,所以需要設(shè)定一個自旋等待的最大時間。

如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖憾筏,就會導(dǎo)致其它爭用鎖的線程在最大等待時間內(nèi)還是獲取不到鎖嚎杨,這時爭用線程會停止自旋進(jìn)入阻塞狀態(tài)。

JVM 對于自旋周期的選擇氧腰,jdk1.6前這個限度是一定的寫死的磕潮,在1.6后引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋的時間不再是固定的了容贝,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定自脯。線程如果自旋成功了,則下次自旋的次數(shù)會更多斤富,如果自旋失敗了膏潮,則自旋的次數(shù)就會減少。

重量級鎖 Synchronized

Synchronized 的作用相信大家都已經(jīng)非常熟悉了满力;

它可以把任意一個非NULL的對象當(dāng)作鎖:

  1. 作用于方法焕参,鎖住的是對象的實例(this);
  2. 當(dāng)作用于靜態(tài)方法時轻纪,鎖住的是 Class 實例,又因為 Class 的相關(guān)數(shù)據(jù)存儲在永久帶 PermGen(jdk1.8則是 metaspace)叠纷,永久帶是全局共享的刻帚,因此靜態(tài)方法鎖相當(dāng)于類的一個全局鎖,會鎖所有調(diào)用該方法的線程涩嚣;
  3. synchronized 作用于一個對象實例時崇众,鎖住的是所有以該對象為鎖的代碼塊。

偏向鎖

所謂的偏向航厚,就是偏心顷歌,即鎖會偏向于當(dāng)前已經(jīng)占有鎖的線程 。

假如大部分情況是沒有競爭的(某個同步塊大多數(shù)情況都不會出現(xiàn)多線程同時競爭鎖)幔睬,那么可以通過偏向來提高性能眯漩。即在無競爭時,之前獲得鎖的線程再次獲得鎖時麻顶,會判斷是否偏向鎖指向我赦抖,那么該線程將不用再次獲得鎖,直接就可以進(jìn)入同步塊辅肾;如果未指向當(dāng)前線程队萤,則當(dāng)前線程會采用 CAS 操作將 markword 中線程ID設(shè)置為當(dāng)前線程ID,如果 CAS 操作成功宛瞄,則獲取偏向鎖成功浮禾,去執(zhí)行同步代碼塊交胚,如果 CAS 操作失敗份汗,則表示有競爭,獲得偏向鎖的線程被掛起蝴簇,偏向鎖升級為輕量級鎖杯活。

偏向鎖的實現(xiàn)就是將對象頭 markword 的標(biāo)記設(shè)置為偏向,并將線程ID寫入對象頭 markword熬词,當(dāng)其他線程請求相同的鎖時旁钧,偏向模式結(jié)束。

JVM默認(rèn)啟用偏向鎖互拾,使用如下的JVM參數(shù)來設(shè)置偏向鎖:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4
BiasedLockingStartupDelay 表示系統(tǒng)啟動幾秒鐘后啟用偏向鎖歪今。默認(rèn)為4秒,原因在于颜矿,系統(tǒng)剛啟動時寄猩,一般數(shù)據(jù)競爭是比較激烈的,此時啟用偏向鎖會降低性能骑疆。

輕量級鎖

輕量級鎖(Lightweight Locking)本意是為了減少多線程進(jìn)入互斥(mutex)的幾率田篇,并不是要替代互斥替废。

它采用 CAS 嘗試將對象的 markword 更新為指向當(dāng)前線程的指針,在進(jìn)入互斥前泊柬,進(jìn)行補救椎镣。

輕量級鎖是由偏向鎖升級來的,輕量鎖存在的目的是盡可能不用動用操作系統(tǒng)層面的互斥兽赁,因為那個性能會比較差状答。因為 JVM 本身就是一個應(yīng)用,所以希望在應(yīng)用層面上就解決線程同步問題闸氮。

總結(jié)一下就是輕量級鎖是一種快速的鎖定方法剪况,在進(jìn)入互斥之前,使用 CAS 操作來嘗試加鎖蒲跨,盡量不要用操作系統(tǒng)層面的互斥译断,提高了性能。

偏向鎖升級為輕量鎖的步驟為:

  1. 虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間或悲,用于存儲鎖對象目前的 markword 的拷貝孙咪;
  2. 拷貝對象頭中的 markword 復(fù)制到鎖記錄中;
  3. 拷貝成功后巡语,虛擬機將使用 CAS 操作嘗試將對象的 markword 更新為指向 Lock Record 的指針翎蹈;
  4. 如果 CAS 操作失敗,表示存在競爭男公,升級為重量級鎖荤堪,就是操作系統(tǒng)層面的同步方法。在沒有鎖競爭的情況枢赔,輕量級鎖減少互斥產(chǎn)生的性能損耗澄阳。在競爭非常激烈時(輕量級鎖(CAS 操作)總是失敗)踏拜,輕量級鎖會多做很多額外操作碎赢,導(dǎo)致性能下降。

小結(jié)

synchronized 的執(zhí)行過程:

  1. 檢測 markword 里面是不是當(dāng)前線程的ID速梗,如果是肮塞,表示當(dāng)前線程處于偏向鎖。
  2. 如果不是姻锁,則使用 CAS 將 markword 中的線程ID更新為當(dāng)前線程的ID枕赵,如果成功則表示當(dāng)前線程獲得偏向鎖。
  3. 如果失敗位隶,則說明發(fā)生競爭拷窜,撤銷偏向鎖,進(jìn)而升級為輕量級鎖。
  4. 當(dāng)前線程使用 CAS 將對象的 markword 更新為指向 Lock Record 的指針装黑,如果成功副瀑,當(dāng)前線程獲得鎖。
  5. 如果失敗恋谭,表示其他線程競爭鎖糠睡,則升級為重量級鎖。

上面幾種鎖都是 JVM 自己內(nèi)部實現(xiàn)疚颊,當(dāng)我們執(zhí)行 synchronized 同步塊的時候 jvm 會根據(jù)啟用的鎖和當(dāng)前線程的爭用情況狈孔,決定如何執(zhí)行同步操作;

在所有的鎖都啟用的情況下線程進(jìn)入臨界區(qū)時會先去獲取偏向鎖材义,如果已經(jīng)存在偏向鎖了均抽,則會嘗試獲取輕量級鎖。如果輕量級鎖獲取失敗其掂,則啟用自旋鎖油挥,如果自旋也沒有獲取到鎖,則升級為重量級鎖款熬,沒有獲取到鎖的線程阻塞掛起深寥,直到持有鎖的線程執(zhí)行完同步塊喚醒他們;

偏向鎖是在無鎖爭用的情況下使用的贤牛,也就是同步開在當(dāng)前線程沒有執(zhí)行完之前惋鹅,沒有其它線程會執(zhí)行該同步塊,一旦有了第二個線程的爭用殉簸,偏向鎖就會升級為輕量級鎖闰集,如果輕量級鎖自旋到達(dá)閾值后,沒有獲取到鎖般卑,就會升級為重量級鎖武鲁;

鎖優(yōu)化

以上介紹的鎖不是我們代碼中能夠控制的,但是借鑒上面的思想椭微,我們可以優(yōu)化我們自己線程的加鎖操作洞坑;

減少鎖時間

不需要同步執(zhí)行的代碼盲链,能不放在同步快里面執(zhí)行就不要放在同步快內(nèi)蝇率,可以讓鎖盡快釋放;

減少鎖的粒度

它的思想是將物理上的一個鎖刽沾,拆成邏輯上的多個鎖本慕,增加并行度,從而降低鎖競爭侧漓。它的思想也是用空間來換時間锅尘;

java中很多數(shù)據(jù)結(jié)構(gòu)都是采用這種方法提高并發(fā)操作的效率:

ConcurrentHashMap
java中的 ConcurrentHashMap 在 jdk1.8 之前的版本,使用一個 Segment 數(shù)組,Segment繼承自 ReenTrantLock藤违,所以每個 Segment 就是個可重入鎖浪腐,每個 Segment 有一個HashEntry< K,V >數(shù)組用來存放數(shù)據(jù),put 操作時顿乒,先確定往哪個 Segment 放數(shù)據(jù)议街,只需要鎖定這個 Segment,執(zhí)行 put璧榄,其它的 Segment 不會被鎖定特漩;所以數(shù)組中有多少個 Segment 就允許同一時刻多少個線程存放數(shù)據(jù),這樣增加了并發(fā)能力骨杂。

LongAdder
LongAdder 實現(xiàn)思路也類似 ConcurrentHashMap涂身,LongAdder 有一個根據(jù)當(dāng)前并發(fā)狀況動態(tài)改變的 Cell 數(shù)組,Cell 對象里面有一個long類型的 value 用來存儲值;
開始沒有并發(fā)爭用的時候或者是 cells 數(shù)組正在初始化的時候搓蚪,會使用cas來將值累加到成員變量的 base 上蛤售,在并發(fā)爭用的情況下,LongAdder 會初始化 cells 數(shù)組妒潭,在 Cell 數(shù)組中選定一個 Cell 加鎖悍抑,數(shù)組有多少個 cell,就允許同時有多少線程進(jìn)行修改杜耙,最后將數(shù)組中每個 Cell 中的 value 相加搜骡,再加上 base 的值,就是最終的值佑女;cell 數(shù)組還能根據(jù)當(dāng)前線程爭用情況進(jìn)行擴容记靡,初始長度為2,每次擴容會增長一倍团驱,直到擴容到大于等于cpu數(shù)量就不再擴容摸吠,這也就是為什么 LongAdder 比 cas 和 AtomicLong 效率要高的原因,后面兩者都是 volatile+cas 實現(xiàn)的嚎花,他們的競爭維度是1寸痢,LongAdder 的競爭維度為“Cell個數(shù)+1”,為什么要+1紊选?因為它還有一個 base啼止,如果競爭不到鎖還會嘗試將數(shù)值加到 base 上;

LinkedBlockingQueue
LinkedBlockingQueue也體現(xiàn)了這樣的思想兵罢,在隊列頭入隊献烦,在隊列尾出隊,入隊和出隊使用不同的鎖卖词,相對于LinkedBlockingArray只有一個鎖效率要高巩那;

拆鎖的粒度不能無限拆,最多可以將一個鎖拆為當(dāng)前 cup 數(shù)量個鎖即可;

鎖粗化

大部分情況下我們是要讓鎖的粒度最小化即横,鎖的粗化則是要增大鎖的粒度;
在以下場景下需要粗化鎖的粒度:
假如有一個循環(huán)噪生,循環(huán)內(nèi)的操作需要加鎖,我們應(yīng)該把鎖放到循環(huán)外面东囚,否則每次進(jìn)出循環(huán)杠园,都進(jìn)出一次臨界區(qū),效率是非常差的舔庶;

使用讀寫鎖

ReentrantReadWriteLock 是一個讀寫鎖抛蚁,讀操作加讀鎖,可以并發(fā)讀惕橙,寫操作使用寫鎖瞧甩,只能單線程寫;

讀寫分離

CopyOnWriteArrayList 弥鹦、CopyOnWriteArraySet
CopyOnWrite 容器即寫時復(fù)制的容器肚逸。通俗的理解是當(dāng)我們往一個容器添加元素的時候,不直接往當(dāng)前容器添加彬坏,而是先將當(dāng)前容器進(jìn)行 Copy朦促,復(fù)制出一個新的容器,然后新的容器里添加元素栓始,添加完元素之后务冕,再將原容器的引用指向新的容器。這樣做的好處是我們可以對 CopyOnWrite 容器進(jìn)行并發(fā)的讀幻赚,而不需要加鎖禀忆,因為當(dāng)前容器不會添加任何元素。所以 CopyOnWrite 容器也是一種讀寫分離的思想落恼,讀和寫不同的容器箩退。
 CopyOnWrite 并發(fā)容器用于讀多寫少的并發(fā)場景,因為佳谦,讀的時候沒有鎖戴涝,但是對其進(jìn)行更改的時候是會加鎖的,否則會導(dǎo)致多個線程同時復(fù)制出多個副本钻蔑,各自修改各自的啥刻;

使用 cas

如果需要同步的操作執(zhí)行速度非常快矢棚,并且線程競爭并不激烈郑什,這時候使用 cas 效率會更高府喳,因為加鎖會導(dǎo)致線程的上下文切換蒲肋,如果上下文切換的耗時比同步操作本身更耗時,且線程對資源的競爭不激烈,使用 volatiled+cas 操作會是非常高效的選擇兜粘;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末申窘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子孔轴,更是在濱河造成了極大的恐慌剃法,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件路鹰,死亡現(xiàn)場離奇詭異贷洲,居然都是意外死亡,警方通過查閱死者的電腦和手機晋柱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門优构,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雁竞,你說我怎么就攤上這事钦椭。” “怎么了碑诉?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵彪腔,是天一觀的道長。 經(jīng)常有香客問我进栽,道長德挣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任快毛,我火速辦了婚禮盲厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祸泪。我一直安慰自己吗浩,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布没隘。 她就那樣靜靜地躺著懂扼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪右蒲。 梳的紋絲不亂的頭發(fā)上阀湿,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機與錄音瑰妄,去河邊找鬼陷嘴。 笑死,一個胖子當(dāng)著我的面吹牛间坐,可吹牛的內(nèi)容都是我干的灾挨。 我是一名探鬼主播邑退,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼劳澄!你這毒婦竟也來了地技?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤秒拔,失蹤者是張志新(化名)和其女友劉穎莫矗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砂缩,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡作谚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了庵芭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片食磕。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖喳挑,靈堂內(nèi)的尸體忽然破棺而出彬伦,到底是詐尸還是另有隱情,我是刑警寧澤伊诵,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布单绑,位于F島的核電站,受9級特大地震影響曹宴,放射性物質(zhì)發(fā)生泄漏搂橙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一笛坦、第九天 我趴在偏房一處隱蔽的房頂上張望区转。 院中可真熱鬧,春花似錦版扩、人聲如沸废离。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜻韭。三九已至,卻和暖如春柿扣,著一層夾襖步出監(jiān)牢的瞬間肖方,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工未状, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俯画,地道東北人。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓司草,卻偏偏與公主長得像艰垂,于是被迫代替她去往敵國和親泡仗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,654評論 2 354

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