內(nèi)核態(tài):
1.系統(tǒng)中既有操作系統(tǒng)的程序,也有普通用戶程序。為了安全性和穩(wěn)定性,操作系統(tǒng)的程序不能隨便訪
問扒俯,這就是內(nèi)核態(tài)族购。即需要執(zhí)行操作系統(tǒng)的程序就必須轉(zhuǎn)換到內(nèi)核態(tài)才能執(zhí)行
2. 內(nèi)核態(tài)可以使用計算機所有的硬件資源
用戶態(tài):不能直接使用系統(tǒng)資源,也不能改變CPU的工作狀態(tài)陵珍,并且只能訪問這個用戶程序自己的存儲空間!Nナ;ゴ俊!
Java 的線程是映射到操作系統(tǒng)原生線程之上的磕蒲,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)的幫忙留潦,這就要從用戶態(tài)轉(zhuǎn)換到核心態(tài),就相當于工作從上海分部轉(zhuǎn)換到瑞典總部的操作一樣辣往,因此狀態(tài)轉(zhuǎn)換需要花費很多的處理器時間兔院。
比如如下代碼:
value++ 因為被關(guān)鍵字 synchronized 修飾,所以會在各個線程間同步執(zhí)行站削。但是 value++ 消耗的時間很有可能比線程狀態(tài)轉(zhuǎn)換消耗的時間還短坊萝,所以說 synchronized 是 Java 語言中一個重量級的操作。
synchronized 實現(xiàn)原理
要了解 synchronized 的原理需要先理清楚兩件事情:對象頭和 Monitor许起。
對象頭
Java 對象在內(nèi)存中的布局分為 3 部分:對象頭十偶、實例數(shù)據(jù)、對齊填充园细。當我們在 Java 代碼中惦积,使用 new 創(chuàng)建一個對象的時候,JVM 會在堆中創(chuàng)建一個 instanceOopDesc 對象猛频,這個對象中包含了對象頭以及實例數(shù)據(jù)狮崩。
instanceOopDesc 的基類為 oopDesc 類。它的結(jié)構(gòu)如下:
其中 _mark 和 _metadata 一起組成了對象頭睦柴。_metadata 主要保存了類元數(shù)據(jù)爱只,不需要做過多介紹恬试。這里重點看下 _mark 屬性训柴,_mark 是 markOop 類型數(shù)據(jù)幻馁,一般稱它為標記字段(Mark Word)仗嗦,其中主要存儲了對象的 hashCode稀拐、分代年齡德撬、鎖標志位,是否偏向鎖等纤勒。
用一張圖來表示 32 位 Java 虛擬機的 Mark Word 的默認存儲結(jié)構(gòu)如下:
默認情況下,沒有線程進行加鎖操作闸翅,所以鎖對象中的 Mark Word 處于無鎖狀態(tài)坚冀。但是考慮到 JVM 的空間效率记某,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu)液南,以便存儲更多的有效數(shù)據(jù)滑凉,它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間畅姊,如 32 位 JVM 下,除了上述列出的 Mark Word 默認存儲結(jié)構(gòu)外粗合,還有如下可能變化的結(jié)構(gòu):
從圖中可以看出隙疚,根據(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 中每個對象都會有一個對應(yīng)的 ObjectMonitor 對象驳规,這也是 Java 中所有的 Object 都可以作為鎖對象的原因吗购。
那 ObjectMonitor 是如何實現(xiàn)同步機制的呢捻勉?
首先看下 ObjectMonitor 的結(jié)構(gòu):
其中有幾個比較關(guān)鍵的屬性:
當多個線程同時訪問一段同步代碼時研底,首先會進入 _EntryList 隊列中胚想,當某個線程通過競爭獲取到對象的 monitor 后浊服,monitor 會把 _owner 變量設(shè)置為當前線程牙躺,同時 monitor 中的計數(shù)器 _count 加 1,即獲得對象鎖脓恕。
若持有 monitor 的線程調(diào)用 wait() 方法秋茫,將釋放當前持有的 monitor肛著,_owner 變量恢復(fù)為 null, _count 自減 1局荚,同時該線程進入 _WaitSet 集合中等待被喚醒聪建。若當前線程執(zhí)行完畢也將釋放 monitor(鎖)并復(fù)位變量的值金麸,以便其他線程進入獲取 monitor(鎖)揍魂。
實例演示
比如以下代碼通過 3 個線程分別執(zhí)行以下同步代碼塊:
鎖對象是 lock 對象,在 JVM 中會有一個 ObjectMonitor 對象與之對應(yīng)庄蹋。如下圖所示:
分別使用 3 個線程來執(zhí)行以上同步代碼塊章咧。默認情況下赁严,3 個線程都會先進入 ObjectMonitor 中的 EntrySet 隊列中耻矮,如下所示:
假設(shè)線程 2 首先通過競爭獲取到了鎖對象踱承,則 ObjectMonitor 中的 Owner 指向線程 2琢唾,并將 count 加 1懒熙。結(jié)果如下:
上圖中 Owner 指向線程 2 表示它已經(jīng)成功獲取到鎖(Monitor)對象,其他線程只能處于阻塞(blocking)狀態(tài)肢娘。如果線程 2 在執(zhí)行過程中調(diào)用 wait() 操作沙廉,則線程 2 會釋放鎖(Monitor)對象,以便其他線程進入獲取鎖(Monitor)對象,Owner 變量恢復(fù)為 null,count 做減 1 操作促王,同時線程 2 會添加到 WaitSet 集合犀盟,進入等待(waiting)狀態(tài)并等待被喚醒。結(jié)果如下:
然后線程 1 和線程 3 再次通過競爭獲取到鎖(Monitor)對象蝇狼,則重新將 Owner 指向成功獲取到鎖的線程阅畴。假設(shè)線程 1 獲取到鎖,如下:
如果在線程 1 執(zhí)行過程中調(diào)用 notify 操作將線程 2 喚醒迅耘,則當前處于 WaitSet 中的線程 2 會被重新添加到 EntrySet 集合中贱枣,并嘗試重新獲取競爭鎖(Monitor)對象。但是 notify 操作并不會是使程 1 釋放鎖(Monitor)對象颤专。結(jié)果如下:
當線程 1 中的代碼執(zhí)行完畢以后纽哥,同樣會自動釋放鎖,以便其他線程再次獲取鎖對象栖秕。
實際上春塌,ObjectMonitor 的同步機制是 JVM 對操作系統(tǒng)級別的 Mutex Lock(互斥鎖)的管理過程,其間都會轉(zhuǎn)入操作系統(tǒng)內(nèi)核態(tài)簇捍。也就是說 synchronized 實現(xiàn)鎖只壳,在“重量級鎖”狀態(tài)下,當多個線程之間切換上下文時暑塑,還是一個比較重量級的操作吼句。
Java 虛擬機對 synchronized 的優(yōu)化
從 Java 6 開始,虛擬機對 synchronized 關(guān)鍵字做了多方面的優(yōu)化梯投,主要目的就是命辖,避免 ObjectMonitor 的訪問况毅,減少“重量級鎖”的使用次數(shù)分蓖,并最終減少線程上下文切換的頻率 。其中主要做了以下幾個優(yōu)化: 鎖自旋尔许、輕量級鎖么鹤、偏向鎖。
鎖自旋
線程的阻塞和喚醒需要 CPU 從用戶態(tài)轉(zhuǎn)為核心態(tài)味廊,頻繁的阻塞和喚醒對 CPU 來說是一件負擔很重的工作蒸甜,勢必會給系統(tǒng)的并發(fā)性能帶來很大的壓力,所以 Java 引入了自旋鎖的操作余佛。實際上自旋鎖在 Java 1.4 就被引入了柠新,默認關(guān)閉,但是可以使用參數(shù) -XX:+UseSpinning 將其開啟辉巡。但是從 Java 6 之后默認開啟恨憎。
所謂自旋,就是讓該線程等待一段時間,不會被立即掛起憔恳,看當前持有鎖的線程是否會很快釋放鎖瓤荔。而所謂的等待就是執(zhí)行一段無意義的循環(huán)即可(自旋)。
自旋鎖也存在一定的缺陷:自旋鎖要占用 CPU钥组,如果鎖競爭的時間比較長输硝,那么自旋通常不能獲得鎖,白白浪費了自旋占用的 CPU 時間程梦。這通常發(fā)生在鎖持有時間長点把,且競爭激烈的場景中,此時應(yīng)主動禁用自旋鎖作烟。
輕量級鎖
有時候 Java 虛擬機中會存在這種情形:對于一塊同步代碼愉粤,雖然有多個不同線程會去執(zhí)行,但是這些線程是在不同的時間段交替請求這把鎖對象拿撩,也就是不存在鎖競爭的情況衣厘。在這種情況下,鎖會保持在輕量級鎖的狀態(tài)压恒,從而避免重量級鎖的阻塞和喚醒操作影暴。
要了解輕量級鎖的工作流程,還是需要再次看下對象頭中的 Mark Word探赫。上文中已經(jīng)提到型宙,鎖的標志位包含幾種情況:00 代表輕量級鎖、01 代表無鎖(或者偏向鎖)伦吠、10 代表重量級鎖妆兑、11 則跟垃圾回收算法的標記有關(guān)。
當線程執(zhí)行某同步代碼時毛仪,Java 虛擬機會在當前線程的棧幀中開辟一塊空間(Lock Record)作為該鎖的記錄搁嗓,如下圖所示:
然后 Java 虛擬機會嘗試使用 CAS(Compare And Swap)操作,將鎖對象的 Mark Word 拷貝到這塊空間中箱靴,并且將鎖記錄中的 owner 指向 Mark Word腺逛。結(jié)果如下:
當線程再次執(zhí)行此同步代碼塊時,判斷當前對象的 Mark Word 是否指向當前線程的棧幀衡怀,如果是則表示當前線程已經(jīng)持有當前對象的鎖棍矛,則直接執(zhí)行同步代碼塊;否則只能說明該鎖對象已經(jīng)被其他線程搶占了抛杨,這時輕量級鎖需要膨脹為重量級鎖够委。
輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合怖现,就會導(dǎo)致輕量級鎖膨脹為重量級鎖茁帽。
偏向鎖
輕量級鎖是在沒有鎖競爭情況下的鎖狀態(tài),但是在有些時候鎖不僅存在多線程的競爭,而且總是由同一個線程獲得脐雪。因此為了讓線程獲得鎖的代價更低引入了偏向鎖的概念厌小。偏向鎖的意思是如果一個線程獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他線程來競爭鎖战秋,那么持有偏向鎖的線程再次進入或者退出同一個同步代碼塊璧亚,不需要再次進行搶占鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking 開啟或者關(guān)閉脂信。
偏向鎖的具體實現(xiàn)就是在鎖對象的對象頭中有個 ThreadId 字段癣蟋,默認情況下這個字段是空的,當?shù)谝淮潍@取鎖的時候狰闪,就將自身的 ThreadId 寫入鎖對象的 Mark Word 中的 ThreadId 字段內(nèi)疯搅,將是否偏向鎖的狀態(tài)置為 01。這樣下次獲取鎖的時候埋泵,直接檢查 ThreadId 是否和自身線程 Id 一致幔欧,如果一致,則認為當前線程已經(jīng)獲取了鎖丽声,因此不需再次獲取鎖礁蔗,略過了輕量級鎖和重量級鎖的加鎖階段。提高了效率雁社。
其實偏向鎖并不適合所有應(yīng)用場景, 因為一旦出現(xiàn)鎖競爭浴井,偏向鎖會被撤銷,并膨脹成輕量級鎖霉撵,而撤銷操作(revoke)是比較重的行為磺浙,只有當存在較多不會真正競爭的 synchronized 塊時,才能體現(xiàn)出明顯改善徒坡;因此實踐中撕氧,還是需要考慮具體業(yè)務(wù)場景,并測試后崭参,再決定是否開啟/關(guān)閉偏向鎖呵曹。
對于鎖的幾種狀態(tài)轉(zhuǎn)換的源碼分析款咖,可以參考:源碼分析Java虛擬機中鎖膨脹的過程