互斥鎖是最常見的同步手段,在併發過程中,當多條執行緒對同一個共享資料競爭時,它保證共享資料同一時刻只能被一條執行緒使用,其他執行緒只有等到鎖釋放後才能重新進行競爭。
對於Java開發人員,最熟悉的肯定就是用synchronized關鍵詞完成鎖功能,在涉及到多執行緒併發時,對於一些變數,你應該會毫不猶豫地加上synchronized去保證變數的同步性。
在C/C++中可直接使用作業系統提供的互斥鎖實現同步和執行緒的阻塞和喚起,與之不同的是,Java要把這些底層封裝,而synchronized就是一個典型的互斥鎖,同時它也是一個JVM級別的鎖,它的實現細節全部封裝在JVM中實現,對開發人員只提供了synchronized關鍵詞。
根據鎖的顆粒度,可以用synchronized對一個變數、一個方法、一個物件和一個類等加鎖。被synchronized修飾的程式塊經過編譯後,會在前後生成monitorenter和monitorexit兩個位元組碼指令,其中涉及到鎖定和解鎖物件的確定,這就要根據synchronized來確定了,假如明確指定了所物件,例如synchronized(變數)、synchronized(this)等,說明加解鎖物件為變數或執行時物件。假如沒有明確指定物件,則根據synchronized修飾的方法去找對應的鎖物件,如修飾一個非靜態方法表示此方法對應的物件為鎖物件,如修飾一個靜態方法則表示此方法對應的類物件為鎖物件。當一個物件被鎖住時,物件裡面所有用synchronized修飾的方法都將產生堵塞,而物件裡非synchronized修飾的方法可正常被呼叫,不受鎖影響。
為了實現互斥鎖,JVM的monitorenter和monitorexit位元組碼依賴底層作業系統的互斥鎖來實現,Java層面的執行緒與作業系統的原生執行緒有對映關係,這時如果要將一個執行緒進行阻塞或喚起都需要作業系統的協助,需要從使用者態切換到核心態來執行,這種切換代價十分昂貴,需要消耗很多處理器時間。如果可能,應該減少這樣的切換,JVM一般會採取一些措施進行優化,例如在把執行緒進行阻塞操作之前先讓執行緒自旋等待一段時間,可能在等待期間其他執行緒已經解鎖,這時就無需再讓執行緒執行阻塞操作,避免了使用者態到核心態的切換。
Synchronized還有另外一個重要的特性——可重入性。這個特性主要是針對當前執行緒而言的,可重入即是自己可以再次獲得自己的內部鎖,在嘗試獲取物件鎖時,如果當前執行緒已經擁有了此物件的鎖,則把鎖的計數器加一,在釋放鎖時則對應地減一,當鎖計數器為0時表示鎖完全被釋放,此時其他執行緒可對其加鎖。可重入特性是為了解決自己鎖死自己的情況,如下面虛擬碼:
public class DeadLock{
public synchronized void method1(){}
public synchronized void method2(){
this.method1();
}
public static void main(String[] args){
DeadLock deadLock=new DeadLock();
deadLock.method2();
}
}
複製程式碼
這種情況其實也並非不常見,一個類中的同步方法呼叫另一個同步方法,假如synchronized不支援重入,進入method2方法時當前執行緒將嘗試獲取deadLock物件的鎖,而method2方法裡面執行method1方法時,當前執行緒又要去嘗試獲取deadLock物件的鎖,這時由於不支援重入,它要去等deadLock物件的鎖釋放,把自己阻塞了,這就是自己鎖死自己的現象。所以重入機制的引入,杜絕了這種情況的發生。
synchronized實現的是一個非公平鎖,非公平主要表現在獲取鎖的行為上,並非是按照申請鎖的時間前後給等待執行緒分配鎖的,每當鎖被釋放後,任何一個執行緒都有機會競爭到鎖,這樣做的目的是為了提高執行效能,當然也會產生執行緒飢餓現象。
synchronized最後一個特性(缺點)就是不可中斷性,在所有等待的執行緒中,你們唯一能做的就是等,而實際情況可能是有些任務等了足夠久了,我要取消此任務去幹別的事情,此時synchronized是無法幫你實現的,它把所有實現機制都交給了JVM,提供了方便的同時也體現出了自己的侷限性。
————-推薦閱讀————
——————廣告時間—————-
跟我交流,向我提問:
公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
歡迎關注: