理解鎖在Java虛擬機中的實現(xiàn)和優(yōu)化之前院崇,我們需要知道對象頭和鎖的關(guān)系嘁信。
在Java虛擬機的實現(xiàn)中每個對象都有一個對象頭尤蛮,用于保存對象的系統(tǒng)信息呐馆。對象頭中有一個稱為Mark Word的部分狞尔,
它是實現(xiàn)鎖的關(guān)鍵丛版,在32位系統(tǒng)中,Mark Word為32個比特位偏序,64位系統(tǒng)中页畦,為64個比特位。它是一個多功能的數(shù)據(jù)區(qū)研儒,
可以存放對象的哈希值豫缨,對象的年齡独令,鎖的指針等信息,一個對象是否占用鎖好芭,占用哪個鎖燃箭,就記錄在這個Mark Word中。
以32位系統(tǒng)為例舍败,普通對象的對象頭如下所示
hash:25 ----------------->| age:4 biased_lock:1 lock:2
它表示Mark Word中有25個比特位表示對象的哈希值招狸,4個比特位表示對象年齡,1個比特位表示是否為“偏向鎖”邻薯,2個比特位表示鎖的信息裙戏。
偏向鎖
偏向鎖是JDK 1.6 提出的一種鎖優(yōu)化方式。其核心思想是厕诡,如果程序沒有競爭累榜,則取消之前已經(jīng)取得鎖的線程同步操作。
也就說灵嫌,若某一鎖被線程獲取后壹罚,便進入偏向模式,當(dāng)線程再次請求這個鎖時醒第,無需進行相關(guān)的同步操作渔嚷,從而節(jié)省了操作時間。
如果在此之前有其他線程進行了鎖請求稠曼,則鎖退出偏向模式形病。
在JVM中使用-XX:+UseBiasedLocking可以設(shè)置啟用偏向鎖。
當(dāng)鎖對象處于偏向模式時霞幅,對象頭會記錄獲取鎖的線程
[JavaThread* | epoch | age | 1 | 10]
前23位表示持有偏向鎖的線程漠吻,后續(xù)2位表示偏向鎖的時間戳,4個比特位表示對象年齡司恳,
年齡后1位固定為1途乃, 表示偏向鎖,最后2位表示可偏向/未鎖定扔傅。
這樣耍共,當(dāng)該線程再次嘗試獲得鎖時,通過Mark Word的線程信息就可以判斷當(dāng)前線程是否持有偏向鎖猎塞。
偏向鎖在鎖競爭激烈的場合沒有太強的優(yōu)化效果试读,因為大量的競爭會導(dǎo)致持有鎖的線程不停地切換,鎖也很難一直保持在偏向模式荠耽,
此時钩骇,使用鎖偏向不僅得不到性能的優(yōu)化,反而有可能降低系統(tǒng)性能。
輕量級鎖
如果偏向鎖失敗倘屹,Java虛擬機會讓線程申請輕量級鎖银亲。
輕量級鎖在虛擬機內(nèi)部,使用一個稱謂BasicObjectLock的對象實現(xiàn)纽匙,這個對象內(nèi)部由一個BasicLock對象和一個持有該鎖的Java對象指針組成务蝠。
BasicObjectLock對象放置在Java棧的棧幀中。在BasicLock對象內(nèi)部還維護者displaced_header字段哄辣,他用于備份對象頭部的Mark Word请梢。
當(dāng)一個線程持有一個對象的鎖時,對象頭部Mark Word如下:
[prt | 00] locked
末尾兩位比特為00力穗,整個Mark Word為指向BasicLock對象的指針。
由于BasicObjectLock對象在線程棧中气嫁,因此該指針必然指向持有該鎖的線程椀贝埃空間。
當(dāng)需要判斷某一線程是否持有該對象鎖時寸宵,也只需簡單的判斷對象頭的指針是否在當(dāng)前線程的棧地址范圍即可崖面。
同時,BasicLock對象的displaced_header字段梯影,備份了元對象的Mark Word內(nèi)存巫员。BasicObjectLock對象的obj字段則指向該對象。
在虛擬機的實現(xiàn)中甲棍,有關(guān)輕量級加鎖的代碼實現(xiàn)可讀性較好简识,這里給出其核心實現(xiàn)代碼:
markOop mark = obj->mark();
lock->set_displaced_header(mark)
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, _addr(), mark)){
TEVENT (slow_enter: release stacklock);
return;
}
首先, BasicLock通過set_displaced_header()方法備份了元對象的Mark Word感猛。 接著使用CAS操作七扰,嘗試將BasicLock的地址復(fù)制到對象頭的Mark Word。
如果復(fù)制成功陪白,那么加鎖成功颈走,否則認為加鎖失敗。如果加鎖失敗咱士,那么輕量級鎖就有可能被膨脹為重量級鎖立由。
鎖膨脹
當(dāng)輕量級鎖失敗,虛擬機就會使用重量級鎖序厉。在使用重量級鎖時锐膜,對象的Mark Word如下:
[prt | 10] monitor
末尾的2比特標(biāo)記位被置為10。整個Mark Word表示指向monitor對象的指針脂矫。
在輕量級鎖處理失敗后枣耀,虛擬機會執(zhí)行以下操作:
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj()) -enter(THREAD);
第一步是廢棄前面BasicLock備份的對象頭信息,第二步則正式啟用重量級鎖。啟用過程分為兩步:
- 通過inflate方法進行鎖膨脹捞奕,其目的是獲得對象的ObjectMonitor
然后使用enter()方法嘗試進入該鎖 - 在enter()方法的調(diào)用中牺堰,線程很可能會在操作系統(tǒng)層面被掛起。如果這樣颅围,線程間切換和調(diào)度的成本就會比較高伟葫。
自旋鎖
鎖膨脹后,進入ObjectMonitor的enter()院促,線程很可能會在操作系統(tǒng)層面被掛起筏养,這樣線程上下文切換的性能損失就比較大。
因此常拓,在鎖膨脹后渐溶,虛擬機會做最后的爭取,希望線程可以盡快進入臨界區(qū)而避免被操作系統(tǒng)掛起弄抬。
一種較為有效的手段就是使用自旋鎖茎辐。
自旋鎖可以使線程在沒有取得鎖時,不被掛起掂恕,而轉(zhuǎn)而去執(zhí)行一個空循環(huán)(即所謂的自旋)拖陆,
在若干個空循環(huán)后,線程如果可以獲得鎖懊亡,則繼續(xù)執(zhí)行依啰。若線程依然不能獲得鎖烟具,才會被掛起斤程。
使用自旋鎖后,線程被掛起的幾率相對減少强霎,線程執(zhí)行的連貫性相對加強艰争。
因此坏瞄,對于那些鎖競爭不是很激烈,鎖占用時間很短的并發(fā)線程甩卓,具有一定的積極意義鸠匀,
但對于鎖競爭激烈,單線程鎖占用時間長的并發(fā)程序逾柿,自旋鎖在自旋等待后缀棍,往往依然無法獲得對應(yīng)的鎖,
不僅僅白白浪費了CPU時間机错,最終還是免不了執(zhí)行被掛起的操作爬范,反而浪費了系統(tǒng)資源。
在JDK 1.6 中弱匪,Java虛擬機提供-XX:+UseSpinning參數(shù)來開啟自旋鎖青瀑,使用-XX:PreBlockSpin參數(shù)來設(shè)置自旋鎖的等待次數(shù)。
在JDK 1.7中,自旋鎖的參數(shù)被取消斥难,虛擬機不再支持由用戶配置自旋鎖枝嘶。自旋鎖總是會執(zhí)行,自旋次數(shù)也由虛擬機自行調(diào)整哑诊。
鎖消除
鎖消除是Java虛擬機在JIT編譯時群扶,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖镀裤。通過鎖消除竞阐,可以節(jié)省毫無意義的請求鎖時間。
鎖消除主要針對在Java開發(fā)過程中暑劝,開發(fā)人員必然會使用一些JDK內(nèi)置的API, 必然StringBuffer, Vector等骆莹。
這些常用的工具類可能會被大面積使用。雖然這些工具類本上可能有對應(yīng)的非線程安全版本担猛,必然StringBuilder, ArrayList等汪疮,
但是開發(fā)人員也很可能在完全沒有多少線程競爭的場合中使用它們。
在這種情況下毁习,這些工具類內(nèi)部的同步方法就是不必要的。虛擬機可以在運行時卖丸,基于逃逸分析技術(shù)纺且,捕獲這些不可能存在競爭,卻有申請鎖的代碼段稍浆,
并消除這些不必要的鎖载碌,從而提高系統(tǒng)性能。
逃逸分析和鎖消除分別可以使用參數(shù)-XX:+DoEscapeAnalysis和-XX:+EliminateLocks開啟(鎖消除必須工作在-server模式下