java線程是映射到操作系統的原生線程之上的,若是要喚醒或阻塞一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到內核態中,這種轉換需要耗費很多的處理時間,有可能比用戶代碼執行時間都要長。
爲了避免以上情況出現,一般能不用鎖就不用鎖,若是不得不使用的情況下,可以使用偏向鎖、自旋鎖等。
1、重量級鎖
鎖的意義是什麼呢?
當數據被多個線程共享時,爲了保證數據的正確性,避免競爭導致數據錯誤,這時就必須使用鎖,讓數據同時只能被一個線程得到並處理。
重量級鎖在JVM中又叫對象監視器(Monitor),它包含了一個競爭鎖的隊列和一個信號阻塞隊列,前者負責做互斥,後者負責線程同步。
重量級鎖中的掛起線程和恢復線程,都需要從用戶態轉換到內核態去執行,這其中需要耗費很多時間,對性能有很大的影響。
2、自旋鎖
其實鎖在大部分情況下會在短時間內執行完成,佔用鎖的時間不會太長,基於這種情況,自旋鎖出現了。
自旋鎖顧名思義,是採用讓當前線程不停地循環去獲取鎖,而不是讓線程掛起,這樣就少了喚醒線程的消耗。
在當前線程不停地獲取鎖時,會佔用CPU時間,增加CPU的消耗,在多核處理器上優勢比較明顯,若鎖保持的時間不長,線程競爭不激烈,自旋鎖是很不錯的。
相反,若鎖被長期佔用,線程數也不斷增加,就變成了“忙式等待”,這時自旋鎖的性能就會下降。
爲了控制自旋鎖循環的次數,JDK1.5默認自旋鎖循環10次若還未得到鎖就升級爲重量級鎖,在JDK1.6中更是引入了自適應自旋鎖,這種簡單來說,就是若自旋成功率高,就會讓線程經過較長時間的自旋,比如自旋100次;若自旋失敗率高,就會讓線程自旋次數很少,甚至不自旋,直接就升級爲重量級鎖了。
3、偏向鎖
不論是重量級鎖還是自旋鎖,在進入或退出時,都得通過CAS來修改對象頭的Mark Word來進行加鎖和釋放鎖。
針對對象頭,一般包含兩部分,第一部分用戶存儲對象自身的運行時數據(Mark Word),比如hashcode、GC分代年齡、鎖狀態標記等,另外一部分爲類型指針(Klass),即對象指向它的類元數據指針。若對象爲數據,則還會有一部分,用戶記錄數據長度的數據。
在一些情況下,總是同一個線程獲取到鎖,此時每次都得通過CAS修改對象頭的Mark Word,顯然是比較多餘的,由此就產生了偏向鎖。
在JDK1.6中引入了偏向鎖,當一個線程獲取到鎖的時候,會在對象頭中放入偏向線程的ID,當下次這個線程再獲取此鎖的時候,就會直接檢查線程ID是否一致,若是一致就說明獲取到鎖了,不用再進行CAS操作,若是不一致,則去檢測對象頭中Mark Word中的偏向鎖標記是否爲1,若是,則嘗試使用CAS修改對象頭中的偏向線程ID爲當前線程,若不是,則使用CAS競爭鎖。
偏向鎖使用了一種競爭纔會釋放鎖的機制,也就是隻有其他線程競爭偏向鎖時,持有偏向鎖的線程纔有可能釋放鎖。
偏向鎖在JDK1.6和1.7中都是默認開啓的,但是需要在程序啓動幾秒之後纔會自動開啓,若是不想延遲開啓,可以修改參數-XX:BiasedLockingStartupDelay=0
現在項目使用多線程的情況比較常見,競爭鎖也是比較激烈,每次都是從偏向鎖升級到輕量級鎖,而偏向鎖用得比較少,所以你也可以關閉偏向鎖,直接默認進入輕量鎖(自旋鎖)狀態,使用參數-XX:-UseBiasedLocking=false即可
對於鎖來說,還有一些其他優化,如鎖削除和鎖膨脹。
鎖削除就是虛擬機即時編譯器(JIT)運行時,依據逃逸分析的數據檢測到不可能存在競爭鎖,就自動把鎖削除了,比如局部變量作爲鎖等。
鎖膨脹,簡單來說就是頻繁執行中對同一對象加鎖,反覆的加鎖和解鎖勢必會帶來很大的性能消耗,這時會自動將鎖的範圍擴展到頻繁執行的外部,將反覆鎖合併爲一個鎖。
4、鎖升級
以上鎖在使用過程中,會自動進行升級,升級的順序:偏向鎖->自旋鎖→重量級鎖
對於鎖升級過程,可以參考以下流程
1、檢測Mark Word裏面是不是當前線程ID,若是,表示當前線程處於偏向鎖,並獲取到鎖
2、若不是,則嘗試使用CAS修改Mark Word置爲當前線程ID,若修改成功,則當前線程獲得偏向鎖,並置偏向位爲1
3、若修改失敗,則說明發生了競爭,撤銷偏向鎖,升級爲輕量級鎖
4、當前線程通過CAS修改Mark Word爲鎖記錄指針,如果成功,則獲取到鎖
5、若失敗,表示有其他線程在競爭鎖,當前線程便使用自旋鎖嘗試獲取鎖
6、若自旋成功,則依然處於輕量級鎖
7、若自旋失敗,則升級爲重量級鎖,線程開始阻塞
5、鎖的優缺點對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比,僅存在納秒級的差距 | 如果線程間存在鎖金正,會額外帶來鎖撤銷的消耗 | 適用只有一個線程訪問同步塊的的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度 | 若線程始終獲取不到鎖,自旋會白白浪費資源,消耗CPU | 追求響應時間,鎖佔用時間很短的情況 |
重量級鎖 | 線程競爭不使用自旋,不消耗CPU | 線程阻塞,響應時間變慢,掛起和喚醒比較消耗資源 | 追求吞吐量,鎖佔用時間較長 |