微信公衆號: IT一刻鐘
大型現實非嚴肅主義現場
一刻鐘與你分享優質技術架構與見聞,作一個有劇情的程序員
關注可第一時間瞭解更多精彩內容,按期有福利相送喲。
蜀國有一個皇帝叫蜀道難,他比較難伺候,別的皇帝早朝都是在大殿上同時接見全部大臣,共商國是。他不同,他說早朝大家不要有事沒事都跑過來嘰嘰喳喳,有事則來,無事則該幹啥幹啥去,而後安排太監天天早上在大門口守着,每次只容許一個大臣進來彙報狀況。
「你敢多放進來一個就砍腦殼的幹活。「
太監趕忙下跪,說「謫!「。
第一天,太監傳話欽天監求見,皇帝允了,欽天監上殿報曰:」臣稟報,昨日我司夜觀星象,西方忽現王星忽明忽暗,恐戎狄那邊有亂。「
「朕知道了,退下吧」。一日無事。
次日,太監傳話欽天監求見,皇帝允了。一日無事。
第三天,太監傳話欽天監求見......一日無事。
第四天,欽天監......一日無事。
第五天,皇帝不耐煩了,和賈太監說,欽天監這老傢伙成天是否是閒着沒事,之後他來了不用給我稟報,直接放他上殿講,講完讓他走吧。
國泰民安的日子依舊過着,天天只有欽天監一我的來報告,賈太監每次看到是欽天監來了,也懶得搭理了,直接放他進去了。(這就是偏向鎖,稍後我細細道來)
又一日,欽天監如往常進殿報道,賈太監站在門口打着盹,突然耳邊傳來一個聲音:
「賈太監,幫我稟告聖上,工部李尚書求見。」
「emmm...進去吧...嗯?等等,尚書大人你先等等,欽天監在裏面,你等會再來求見吧。」太監一陣後怕,尋思着欽天監還在裏面呢,這要是放進去了,我這腦殼可就沒了,果真嗜睡誤事。
過了一下子,李尚書回來詢問求見,被告知欽天監還沒走,只好又離去。
又過了一下子,李尚書又回來詢問求見,正巧欽天監走了,太監進殿傳話說工部李尚書求見,皇帝宣覲見,李尚書進殿上報了一番東南連連大雨,已派人去監察水利,修繕河堤。(這就是輕量級鎖)
忽一日,西戎狄和北匈奴同時對帝國西方和北方發難,前線戰事消息如片片雪花紛紛涌入京城,瞬間殿外來了一羣大臣有要事稟告。
一下子這個來問賈公公我能夠進去了嗎?一下子那個來問賈公公我能夠進去了嗎?
把賈太監累的喲,一天下來光說「稍後再來」都把嘴皮子磨破了,沒幾日,賈太監就跪在皇帝面前哭泣道:「聖上啊,快想一想辦法呀,奴才這身子骨就要交代在門口了。」
皇帝一聽,說你傻啊,叫他們一個個在門外排隊啊,誰叫你要他們稍後來求見的。
賈太監細思大喜,以爲有理,第二天在門口豎起一個牌子「稟報要事者,這邊排隊」,賈太監不再用一我的對着一羣人反覆回話,只須要每次出來一個,而後傳話放進去一個,就能夠了。(這就是重量級鎖)
上面這個故事,分別講述了synchronized內部四種級別的狀態,分別是:無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態。程序員
咱們首先從重量級鎖開始講,重量級鎖是經過互斥量(Mutex)來實現的,即一個線程進入了synchronized同步塊,在未完成任務時,會阻塞後面的全部線程。
就像上面的故事所講的,要稟告要事的大臣只能在大殿門口外一個接一個的阻塞排隊。
之因此稱它爲重量級鎖,是由於Java線程是映射到操做系統的原生線程上的,若是要阻塞或喚醒一個線程,都須要依靠操做系統從當前用戶態轉換到核心態中,這種狀態轉換須要耗費處理器不少時間,對於簡單同步塊,可能狀態轉換時間比用戶代碼執行時間還要長,致使實際業務處理所佔比偏小,性能損失較大。
固然這個在虛擬機層面進行了一些好比自旋等待,鎖粗化等等的優化,避免陷入頻繁的切換狀態。在這裏我就不細講了,有興趣的能夠關注我,我後續再和各位看官講上一講。數組
輕量級鎖是JDK6引入的,它的輕量是相較於經過系統互斥量實現的傳統鎖,輕量鎖並非用來取代重量級鎖的,而是在沒有大量線程競爭的狀況下,減小系統互斥量的使用,下降性能的損耗。
輕量級鎖是經過CAS(Compare And Swap)機制實現的,即若是鎖被其餘線程所佔用,當前線程會經過自旋來獲取鎖,從而避免用戶態與核心態的轉換。
就像上面故事所說的,大殿中欽天監在彙報工做,工部尚書要求見,並不須要賈太監每次都進去問一下皇帝,惹得皇帝龍顏大怒,而是大臣本身隔一段時間便來詢問賈太監能不能進去,不能就稍後再來問,直到能夠進去爲止。微信
偏向鎖也是JDK6引入的,它存在的依據是「大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到」。它是經過記錄第一次進入同步塊的線程id來實現的,若是下一個要進入同步塊的線程與記錄的線程id相同,則說明這個鎖由此線程佔有,能夠直接進入到同步塊,不用執行CAS。
就像故事中的,若是天天只有欽天監一我的來的話,就不用賈太監稟告了,賈太監每次一看到欽天監,尋思着,喲,欽天監呢,您自個兒直接進去吧,說完自個兒出來吧。
若是說輕量鎖是爲了消除系統互斥量帶來的性能損耗,那麼偏向鎖就是爲了消除CAS帶來的性能損耗,使之在無競爭的狀況下消除整個同步,性能無限接近非同步。數據結構
要說這個問題,咱們須要先講一下Java對象頭,每一個對象都會有一個對象頭,它分爲三個部分:多線程
內容 | 說明 |
---|---|
Mark Word | 存儲對象的hashcode或鎖信息 |
Class Metadata Address | 存儲到對象類型數據的指針 |
Array length | 數組的長度(若是當前對象是數組) |
從表格可見,synchronized鎖的信息是存在對象頭裏一個叫Mark Word的區域裏的,考慮到虛擬機的空間效率,Mark Word被設計成非固定的數據結構,會根據對象的狀態複用存儲空間來存儲不一樣的內容:
架構
當JVM啓用了偏向鎖模式(JDK6以上默認開啓),新建立對象的Mark Word是未鎖定,未偏向但可偏向狀態,此時Mark Word中的Thread id爲0,表示未偏向任何線程,也叫作匿名偏向(anonymously biased)。性能
當第一個線程嘗試進入同步塊時,發現Mark Word中線程ID爲0,則會使用CAS將本身的線程ID設置到Mark Word中,而且,在當前線程棧中由高到低順序找到可用的Lock Record,將線程ID記錄下。完成這些,此線程就獲取了鎖對象的偏向鎖。
當該偏向線程再次進入同步塊時,發現鎖對象偏向的就是當前線程,會往當前線程的棧中添加一條Displaced Mark Word爲空的Lock Record中,用來統計重入的次數,而後繼續執行同步塊代碼,由於線程棧是私有的,不須要CAS指令進行操做,因此在偏向鎖模式下,同一個線程,只會執行一個CAS,以後獲取釋放鎖只須要對Lock Record作操做,性能損耗基本能夠忽略。
當另一個線程試圖進入同步塊時,發現Mark Word中線程ID與本身不相符,這個時候就會引起偏向鎖的撤銷,變成無鎖不可偏向狀態或輕量級鎖狀態,固然,這只是宏觀上的描述,嚴格意義上講是不許確的,由於裏面還存在重偏向機制,這裏就不過於深刻,在後續的文章中,我會專門出一篇文章,給各位看官詳細介紹偏向鎖究竟是怎麼回事。優化
當鎖對象變成無鎖不可偏向狀態時,多個線程運行到同步塊之後,會檢查鎖對象狀態值標誌是否加鎖,若是沒有鎖,就把鎖對象的Mark Word信息拷貝存儲到當前線程棧楨中Lock Record裏,而後經過CAS嘗試把對象的Mark Word的值改變成一個指向本身線程的指針。若是成功,則當前線程得到鎖對象的輕量級鎖,其餘線程的CAS就會失敗,由於鎖對象的Mark Word已經變成一個新的指針了,必須等待線程釋放鎖,此時其餘線程則經過自旋來競爭鎖。當獲取鎖的線程執行完畢釋放鎖的時候,會將Lock Record裏面以前拷貝的值還原到鎖對象的Mark Word中。spa
當自旋次數超過JVM預期上限,會影響性能,因此競爭的線程就會把鎖對象的Mark Word指向重鎖,所謂的重鎖,實際上就是一個堆上的monitor對象,即,重量級鎖的狀態下,對象的Mark Word爲指向一個堆中monitor對象的指針。
而後全部的競爭線程放棄自旋,逐個插入到monitor對象裏的一個隊列尾部,進入阻塞狀態。
當成功獲取輕量級鎖的線程執行完畢,嘗試經過CAS釋放鎖時,由於Mark Word已經指向重鎖,致使輕量級鎖釋放失敗,這時線程就會知道鎖已經升級爲重量級鎖, 它不只要釋放當前鎖,還要喚醒其餘阻塞的線程來從新競爭鎖。
大概流程以下圖所示:
這裏有一點需注意的是:鎖只能升級,不能降級。操作系統
鎖 | 優勢 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不須要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 | 若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊場景 |
輕量級鎖 | 競爭的線程不會堵塞,提升了程序的響音速度 | 始終得不到鎖的線程,使用自旋會消耗CPU | 追求響應時間,同步塊執行速度很是快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量,同步塊執行速度較慢 |
synchronized無非如下兩種:
1.對象鎖:修飾非靜態方法,修飾代碼塊
2.類鎖:修飾靜態方法,修飾代碼塊
其中按照修飾類型來分,又能夠分爲代碼塊同步和方法同步
代碼塊同步鎖的是對象,使用monitorenter和monitorexit指令實現的。雖然我知道多一行代碼少一位看官的定理,可是這裏仍是必須貼一張代碼圖,來證實我沒有瞎說,是有理有據的「理據服」。
想要降服妖怪,就得先將其打回原形,因此咱們先對一段簡單的代碼進行反編譯,獲得它的字節碼。
final Object lock = new Object(); public int subtr(int i){ synchronized (lock){ return i-1; } }
字節碼:
能夠看出,monitorenter指令是在編譯後插入到同步代碼塊的開始位置,monitorexit插入到同步代碼塊結束的地方,正常狀況下monitorenter和monitorexit是一對一的匹配,然後面又出現了一個monitorexit,是由於那裏是異常處,用來保證方法執行異常的時候,能夠自動解鎖,而不會形成死鎖。
方法同步的實現官方沒有透露,咱們嘗試對一個方法同步的代碼進行反編譯。
public synchronized int add(int i){ return i+1; }
字節碼:
從字節碼裏也看不到monitorenter和monitorexit,智能發現flags那裏,多了一個ACC_SYNCHRONIZED的標示,沒什麼頭緒。不過我猜測,底層應該是鎖方法所屬的對象或類。
這就是synchronized的大體原理,打回原形以後來看,是否是就以爲也不過如此?有什麼疑問或更好的解讀,能夠在下方留言,咱們進行愉快友好的磋商交流。
若是以爲有用,記得分享~