目前在Java中存在兩種鎖機制:synchronized和Lock。數據同步須要依賴鎖,那鎖時如何實現的呢,synchronized給出的答案是軟件層面依賴JVM,而Lock給出的方案是在硬件層面依賴特殊的CPU指令。數據結構
下面先介紹synchronized的實現:併發
synchronized是關鍵字,即便有了Lock接口(Lock可以實現synchronized可以實現的全部功能),使用的仍是很是普遍。其應用層的語義是能夠把任何一個非nul對象做爲鎖,當synchronized做用在方法上時,鎖住的對象是實例對象(this);看成用在靜態方法時鎖住的對象時對象對應的Class實例,由於Class數據存在於永久帶,所以靜態方法鎖至關於類的一個全局鎖;當synchronized做用於某一個對象實例時,鎖住的即是對應的代碼塊。在JVM實現中,全部一個專門的名字:對象監視器函數
線程狀態與狀態的轉換性能
當多個線程同時請求某個對象監視器時,對象監視器會設置幾種狀態來區分請求的線程優化
狀態轉換關係:this
新請求鎖的線程首先被加入到 ContentionList中,當某個擁有鎖的線程(Owner狀態)調用unlock以後,若是發現EntryList爲空則從ContentionList中移動線程到EntryList。spa
自旋鎖操作系統
那些處於ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態,阻塞操做由操做系統完成(在Linxu下經過pthread_mutex_lock函數)。線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能。緩解上述問題的辦法即是自旋,其原理是:當發生爭用時,若Owner線程能在很短的時間內釋放鎖,則那些正在爭用線程能夠稍微等一等(自旋),在Owner線程釋放鎖後,爭用線程可能會當即獲得鎖,從而避免了系統阻塞。但Owner運行的時間可能會超出了臨界值,爭用線程自旋一段時間後仍是沒法得到鎖,這時爭用線程則會中止自旋進入阻塞狀態(後退)。基本思路就是自旋,不成功再阻塞,儘可能下降阻塞的可能性,這對那些執行時間很短的代碼塊來講有很是重要的性能提升。自旋鎖有個更貼切的名字:自旋-指數後退鎖,也即複合鎖。很顯然,自旋在多處理器上纔有意義。還有個問題是,線程自旋時作些啥?其實啥都不作,能夠執行幾回for循環,能夠執行幾條空的彙編指令,目的是佔着CPU不放,等待獲取鎖的機會。因此說,自旋是把雙刃劍,若是旋的時間過長會影響總體性能,時間太短又達不到延遲阻塞的目的。顯然,自旋的週期選擇顯得很是重要,但這與操做系統、硬件體系、系統的負載等諸多場景相關,很難選擇,若是選擇不當,不但性能得不到提升,可能還會降低,所以你們廣泛認爲自旋鎖不具備擴展性。對自旋鎖週期的選擇上,HotSpot認爲最佳時間應是一個線程上下文切換的時間,但目前並無作到。通過調查,目前只是經過彙編暫停了幾個CPU週期,除了自旋週期選擇,HotSpot還進行許多其餘的自旋優化策略,具體以下:
◆ 若是平均負載小於CPUs則一直自旋。
◆ 若是有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞。
◆ 若是正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞。
◆ 若是CPU處於節電模式則中止自旋。
◆ 自旋時間的最壞狀況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差)。
◆ 自旋時會適當放棄線程優先級之間的差別。
那synchronized實現什麼時候使用了自旋鎖?答案是在線程進入ContentionList時,也即第一步操做前。線程在進入等待隊列時首先進行自旋嘗試得到鎖,若是不成功再進入等待隊列。這對那些已經在等待隊列中的線程來講,稍微顯得不公平。還有一個不公平的地方是自旋線程可能會搶佔了Ready線程的鎖。自旋鎖由每一個監視對象維護,每一個監視對象一個。線程
經過上面的介紹能夠看出,synchronized的底層實現主要是依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但得到了高吞吐量。指針