Java 6 對 synchronized 鎖做了多方面的優(yōu)化,其中最主要的就是引入了 偏向鎖和輕量級鎖欺殿。鎖的獲取次序依次是?偏向鎖?->?輕量級鎖?-> 重量級鎖寄纵。
synchronized 實現(xiàn)原理
要了解 synchronized 的原理需要先理清楚兩件事情:對象頭和 Monitor。
對象頭
Java對象在內存中分為三部分:對象頭脖苏、實例數(shù)據(jù)程拭、對齊填充。當我們在 Java 代碼中棍潘,使用 new 創(chuàng)建一個對象的時候恃鞋,JVM 會在堆中創(chuàng)建一個 instanceOopDesc 對象,這個對象中包含了對象頭以及實例數(shù)據(jù)亦歉。
instanceOopDesc 的基類為 oopDesc 類恤浪。它的結構如下:
其中 _mark 和 _metadata 一起組成了對象頭。_metadata 主要保存了類元數(shù)據(jù)肴楷。重點看下 _mark 屬性水由,_mark 是 markOop 類型數(shù)據(jù),一般稱它為標記字段(Mark Word)赛蔫,其中主要存儲了對象的 hashCode砂客、分代年齡泥张、鎖標志位,是否偏向鎖等鞠值。
用一張圖來表示 32 位 Java 虛擬機的 Mark Word 的默認存儲結構如下:
默認情況下媚创,沒有線程進行加鎖操作,所以鎖對象中的 Mark Word 處于無鎖狀態(tài)齿诉。但是考慮到 JVM 的空間效率筝野,Mark Word 被設計成為一個非固定的數(shù)據(jù)結構,以便存儲更多的有效數(shù)據(jù)粤剧,它會根據(jù)對象本身的狀態(tài)復用自己的存儲空間,如 32 位 JVM 下挥唠,除了上述列出的 Mark Word 默認存儲結構外抵恋,還有如下可能變化的結構:
從圖中可以看出,根據(jù)"鎖標志位”以及"是否為偏向鎖"宝磨,Java 中的鎖可以分為以下幾種狀態(tài):
在 Java 6 之前弧关,并沒有輕量級鎖和偏向鎖,只有重量級鎖唤锉,也就是通常所說 synchronized 的對象鎖世囊,鎖標志位為 10。從圖中的描述可以看出:當鎖是重量級鎖時窿祥,對象頭中 Mark Word 會用 30 bit 來指向一個“互斥量”株憾,而這個互斥量就是 Monitor。
Monitor
Monitor 可以把它理解為一個同步工具晒衩,也可以描述為一種同步機制嗤瞎。實際上,它是一個保存在對象頭中的一個對象听系。在 markOop 中有如下代碼:
通過 monitor() 方法創(chuàng)建一個 ObjectMonitor 對象贝奇,而 ObjectMonitor 就是 Java 虛擬機中的 Monitor 的具體實現(xiàn)。因此 Java 中每個對象都會有一個對應的 ObjectMonitor 對象靠胜,這也是 Java 中所有的 Object 都可以作為鎖對象的原因掉瞳。
ObjectMonitor 是如何實現(xiàn)同步機制
首先看下 ObjectMonitor 的結構:
其中有幾個比較關鍵的屬性:
當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 隊列中浪漠,當某個線程通過競爭獲取到對象的 monitor 后陕习,monitor 會把 _owner 變量設置為當前線程,同時 monitor 中的計數(shù)器 _count 加 1郑藏,即獲得對象鎖衡查。
若持有 monitor 的線程調用 wait() 方法,將釋放當前持有的 monitor必盖,_owner 變量恢復為 null拌牲, _count 自減 1俱饿,同時該線程進入 _WaitSet 集合中等待被喚醒。若當前線程執(zhí)行完畢也將釋放 monitor(鎖)并復位變量的值塌忽,以便其他線程進入獲取 monitor(鎖)拍埠。
實例演示
比如以下代碼通過 3 個線程分別執(zhí)行以下同步代碼塊:
鎖對象是 lock 對象,在 JVM 中會有一個 ObjectMonitor 對象與之對應土居。如下圖所示:
分別使用 3 個線程來執(zhí)行以上同步代碼塊枣购。默認情況下,3 個線程都會先進入 ObjectMonitor 中的 EntrySet 隊列中擦耀,如下所示:
假設線程 2 首先通過競爭獲取到了鎖對象棉圈,則 ObjectMonitor 中的 Owner 指向線程 2,并將 count 加 1眷蜓。結果如下:
上圖中 Owner 指向線程 2 表示它已經成功獲取到鎖(Monitor)對象分瘾,其他線程只能處于阻塞(blocking)狀態(tài)。如果線程 2 在執(zhí)行過程中調用 wait() 操作吁系,則線程 2 會釋放鎖(Monitor)對象德召,以便其他線程進入獲取鎖(Monitor)對象,Owner 變量恢復為 null汽纤,count 做減 1 操作上岗,同時線程 2 會添加到 WaitSet 集合,進入等待(waiting)狀態(tài)并等待被喚醒蕴坪。結果如下:
然后線程 1 和線程 3 再次通過競爭獲取到鎖(Monitor)對象肴掷,則重新將 Owner 指向成功獲取到鎖的線程。假設線程 1 獲取到鎖辞嗡,如下:
當線程 1 中的代碼執(zhí)行完畢以后捆等,同樣會自動釋放鎖,以便其他線程再次獲取鎖對象续室。
實際上栋烤,ObjectMonitor 的同步機制是 JVM 對操作系統(tǒng)級別的 Mutex Lock(互斥鎖)的管理過程,其間都會轉入操作系統(tǒng)內核態(tài)挺狰。也就是說 synchronized 實現(xiàn)鎖明郭,在“重量級鎖”狀態(tài)下,當多個線程之間切換上下文時丰泊,還是一個比較重量級的操作薯定。
Java 虛擬機對 synchronized 的優(yōu)化
從 Java 6 開始,虛擬機對 synchronized 關鍵字做了多方面的優(yōu)化瞳购,主要目的就是话侄,避免 ObjectMonitor 的訪問,減少“重量級鎖”的使用次數(shù),并最終減少線程上下文切換的頻率 年堆。其中主要做了以下幾個優(yōu)化: 鎖自旋吞杭、輕量級鎖、偏向鎖变丧。
鎖自旋
線程的阻塞和喚醒需要 CPU 從用戶態(tài)轉為核心態(tài)芽狗,頻繁的阻塞和喚醒對 CPU 來說是一件負擔很重的工作,勢必會給系統(tǒng)的并發(fā)性能帶來很大的壓力痒蓬,所以 Java 引入了自旋鎖的操作童擎。實際上自旋鎖在 Java 1.4 就被引入了,默認關閉攻晒,但是可以使用參數(shù) -XX:+UseSpinning 將其開啟顾复。但是從 Java 6 之后默認開啟。
所謂自旋炎辨,就是讓該線程等待一段時間捕透,不會被立即掛起,看當前持有鎖的線程是否會很快釋放鎖碴萧。而所謂的等待就是執(zhí)行一段無意義的循環(huán)即可(自旋)。
自旋鎖也存在一定的缺陷:自旋鎖要占用 CPU末购,如果鎖競爭的時間比較長破喻,那么自旋通常不能獲得鎖,白白浪費了自旋占用的 CPU 時間盟榴。這通常發(fā)生在鎖持有時間長曹质,且競爭激烈的場景中,此時應主動禁用自旋鎖擎场。
輕量級鎖
有時候 Java 虛擬機中會存在這種情形:對于一塊同步代碼羽德,雖然有多個不同線程會去執(zhí)行,但是這些線程是在不同的時間段交替請求這把鎖對象迅办,也就是不存在鎖競爭的情況宅静。在這種情況下,鎖會保持在輕量級鎖的狀態(tài)站欺,從而避免重量級鎖的阻塞和喚醒操作姨夹。
要了解輕量級鎖的工作流程,還是需要再次看下對象頭中的 Mark Word矾策。上文中已經提到磷账,鎖的標志位包含幾種情況:00 代表輕量級鎖、01 代表無鎖(或者偏向鎖)贾虽、10 代表重量級鎖逃糟、11 則跟垃圾回收算法的標記有關。
當線程執(zhí)行某同步代碼時,Java 虛擬機會在當前線程的棧幀中開辟一塊空間(Lock Record)作為該鎖的記錄绰咽,如下圖所示:
然后 Java 虛擬機會嘗試使用 CAS(Compare And Swap)操作菇肃,將鎖對象的 Mark Word 拷貝到這塊空間中,并且將鎖記錄中的 owner 指向 Mark Word剃诅。結果如下:
當線程再次執(zhí)行此同步代碼塊時巷送,判斷當前對象的 Mark Word 是否指向當前線程的棧幀,如果是則表示當前線程已經持有當前對象的鎖矛辕,則直接執(zhí)行同步代碼塊笑跛;否則只能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖聊品。
輕量級鎖所適應的場景是線程交替執(zhí)行同步塊的場合飞蹂,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖翻屈。
偏向鎖
輕量級鎖是在沒有鎖競爭情況下的鎖狀態(tài)陈哑,但是在有些時候鎖不僅存在多線程的競爭,而且總是由同一個線程獲得伸眶。因此為了讓線程獲得鎖的代價更低引入了偏向鎖的概念惊窖。偏向鎖的意思是如果一個線程獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他線程來競爭鎖厘贼,那么持有偏向鎖的線程再次進入或者退出同一個同步代碼塊界酒,不需要再次進行搶占鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking 開啟或者關閉嘴秸。
偏向鎖的具體實現(xiàn)就是在鎖對象的對象頭中有個 ThreadId 字段毁欣,默認情況下這個字段是空的,當?shù)谝淮潍@取鎖的時候岳掐,就將自身的 ThreadId 寫入鎖對象的 Mark Word 中的 ThreadId 字段內凭疮,將是否偏向鎖的狀態(tài)置為 01。這樣下次獲取鎖的時候串述,直接檢查 ThreadId 是否和自身線程 Id 一致执解,如果一致,則認為當前線程已經獲取了鎖剖煌,因此不需再次獲取鎖材鹦,略過了輕量級鎖和重量級鎖的加鎖階段。提高了效率耕姊。
其實偏向鎖并不適合所有應用場景, 因為一旦出現(xiàn)鎖競爭桶唐,偏向鎖會被撤銷,并膨脹成輕量級鎖茉兰,而撤銷操作(revoke)是比較重的行為尤泽,只有當存在較多不會真正競爭的 synchronized 塊時,才能體現(xiàn)出明顯改善;因此實踐中坯约,還是需要考慮具體業(yè)務場景熊咽,并測試后,再決定是否開啟/關閉偏向鎖闹丐。
最后
本課程主要介紹了 Java 中鎖的幾種狀態(tài)横殴,其中偏向鎖和輕量級鎖都是通過自旋等技術避免真正的加鎖,而重量級鎖才是獲取鎖和釋放鎖卿拴,重量級鎖通過對象內部的監(jiān)視器(ObjectMonitor)實現(xiàn)衫仑,其本質是依賴于底層操作系統(tǒng)的 Mutex Lock 實現(xiàn),操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內核態(tài)的切換堕花,成本非常高文狱。