synchronized用法
在Java中,最簡單粗暴的同步手段就是synchronized關鍵字,其同步的三種用法:
①.同步例項方法,鎖是當前例項物件
②.同步類方法,鎖是當前類物件
③.同步程式碼塊,鎖是括號裡面的物件
示例:
public class SynchronizedTest {
/**
* 同步例項方法,鎖例項物件
*/
public synchronized void test() {
}
/**
* 同步類方法,鎖類物件
*/
public synchronized static void test1() {
}
/**
* 同步程式碼塊
*/
public void test2() {
// 鎖類物件
synchronized (SynchronizedTest.class) {
// 鎖例項物件
synchronized (this) {
}
}
}
}
複製程式碼
synchronized實現
javap -verbose檢視上述示例:
同步方法:方法級同步沒有通過位元組碼指令來控制,它實現在方法呼叫和返回操作之中。當方法呼叫時,呼叫指令會檢查方法ACC_SYNCHRONIZED訪問標誌是否被設定,若設定了則執行執行緒需要持有管程(Monitor)才能執行方法,當方法完成(無論是否出現異常)時釋放管程。
同步程式碼塊:synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit兩個位元組碼指令,每條monitorenter指令都必須執行其對應的monitorexit指令,為了保證方法異常完成時這兩條指令依然能正確執行,編譯器會自動產生一個異常處理器,其目的就是用來執行monitorexit指令(圖中14-18、24-30為異常流程)。
具體看下monitorexit指令做了什麼,在Hotspot原始碼中全文搜尋monitorenter,在ciTypeFlow.cpp中找到如下:
case Bytecodes::_monitorenter:
{
pop_object();
set_monitor_count(monitor_count() + 1);
break;
}
case Bytecodes::_monitorexit:
{
pop_object();
assert(monitor_count() > 0, "must be a monitor to exit from");
set_monitor_count(monitor_count() - 1);
break;
}
void pop_object() {
assert(is_reference(type_at_tos()), "must be reference type");
pop();
}
void pop() {
debug_only(set_type_at_tos(bottom_type()));
_stack_size--;
}
int monitor_count() const { return _monitor_count; }
void set_monitor_count(int mc) { _monitor_count = mc; }
複製程式碼
從原始碼中我們發現當執行緒獲得該物件鎖後,計數器就會加一,釋放鎖就會將計數器減一。
Monitor
每個物件都擁有自己的監視器,當這個物件由同步塊或者這個物件的同步方法呼叫時,執行方法的執行緒必須先獲取該物件的監視器才能進入同步塊和同步方法,如果沒有獲取到監視器的執行緒將會被阻塞在同步塊和同步方法的入口處,進入到BLOCKED狀態,如圖:
Monitor是執行緒私有的資料結構,每個執行緒都有一個可用monitor record列表,同時 還有一個全域性的可用列表。每一個被鎖住的物件都會和一個monitor關聯(物件頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。其結構如下:
Owner:初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖後儲存執行緒唯一標識,當鎖被釋放時又設定為NULL
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的執行緒
RcThis:表示blocked或waiting在該monitor record上的所有執行緒的個數
Nest:用來實現重入鎖的計數
HashCode:儲存從物件頭拷貝過來的HashCode值(可能還包含GC age)
Candidate:用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致效能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的執行緒1表示要喚醒一個繼任執行緒來競爭鎖
鎖優化
jdk1.6中synchronized的實現進行了各種優化,如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖,主要解決三種場景:
①.只有一個執行緒進入臨界區,偏向鎖
②.多執行緒未競爭,輕量級鎖
③.多執行緒競爭,重量級鎖
偏向鎖→輕量級鎖→重量級鎖過程,鎖可以升級但不能降級,這種策略是為了提高獲得鎖和釋放鎖的效率,原始碼解析可以看佔小狼——synchronized實現
偏向鎖
引入偏向鎖的目的是:在沒有多執行緒競爭的情況下,儘量減少不必要的輕量級鎖執行路徑。相對於輕量級鎖,偏向鎖只依賴一次CAS原子指令置換ThreadID,不過一旦出現多個執行緒競爭時必須撤銷偏向鎖,主要校驗是否為偏向鎖、鎖標識位以及ThreadID。
②.檢測Mark Word是否為可偏向狀態,即mark的偏向鎖標誌位為1,鎖標識位為01
③.若為可偏向狀態,判斷Mark Word中的執行緒ID是否為當前執行緒ID,如果指向當前執行緒執行⑥,否則執行④
④.通過CAS操作競爭鎖,競爭成功,則將Mark Word的執行緒ID替換為當前執行緒ID,否則執行⑤
⑤.通過CAS競爭鎖失敗,證明當前存在多執行緒競爭,當到達safepoint全域性安全點(這個時間點是上沒有正在執行的程式碼),獲得偏向鎖的執行緒被掛起,撤銷偏向鎖,並升級為輕量級,升級完成後被阻塞在安全點的執行緒繼續執行同步程式碼塊
⑥.執行同步程式碼塊
①.暫停擁有偏向鎖的執行緒,判斷鎖物件石是否處於被鎖定狀態
②.撤銷偏向鎖,恢復到無鎖狀態(01)或者輕量級鎖(00)的狀態
輕量級鎖
引入輕量級鎖的主要目的是在多執行緒沒有競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。如果多個執行緒在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要替代重量級鎖,在有多執行緒競爭的情況下,輕量級鎖比重量級鎖更慢。
②.判斷當前物件是否處於無鎖狀態,即mark的偏向鎖標誌位為0,鎖標誌位為 01
③.若是,JVM首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),然後執行④;若不是執行⑤
④.JVM利用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,如果成功表示競爭到鎖,則將鎖標誌位變成00(表示此物件處於輕量級鎖狀態),執行同步操作;如果失敗則執行⑤
⑤.判斷當前物件的Mark Word是否指向當前執行緒的棧幀,如果是說明當前執行緒已經持有這個物件的鎖,則直接執行同步程式碼塊;否則說明該鎖物件已經被其他執行緒搶佔了,如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌位變成10,後面等待的執行緒將會進入阻塞狀態
①.如果物件的Mark Word仍然指向著執行緒的鎖記錄,執行②
②.用CAS操作把物件當前的Mark Word和執行緒中複製的Displaced Mark Word替換回來,如果成功,則說明釋放鎖成功,否則執行③
③.如果CAS操作替換失敗,說明有其他執行緒嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的執行緒
重量級鎖
重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。
感謝
芋道原始碼——synchronized實現原理
《深入理解Java虛擬機器》
《java併發程式設計的藝術》