聊聊Synchronized的優化
早期版本synchronized性能較低的原因
在早期版本中,synchronized是一種重量級鎖,其底層由Monitor實現,而Monitor又依賴于操作系統的Mutex Lock。線程獲取到鎖后,需要切換狀態,而操作系統在實現線程的切換時,需要從用戶態轉為核心態,這是一個非常耗時,非常重的操作。因此在之前,synchronized是一種重量級鎖。
JDK1.6之后對synchronized的優化
現在的synchronized已經沒有之前那么笨重了,在虛擬機層面,對synchronized做了較大的優化,引入了自旋鎖、適應性自旋鎖、鎖消除、鎖粗化,可以減少鎖操作的開銷。
自旋鎖
有時候,獲取到鎖的線程執行的操作耗時極短,為了這么點微不足道的時間,將接下來等待鎖的線程掛起非常的不值得。掛起線程的操作需要在核心態完成,從用戶態切換到核心態,耗時比較嚴重。
因此現在增加這么一樣操作,讓等待鎖的線程執行忙循環等待,不停地去嘗試獲取鎖,像一種自旋的操作,故稱之為自旋鎖。
如果之前線程占有鎖的時間極短,那么自旋鎖的性能將非常的好。但若是占有鎖的時間較長,那么自旋鎖將白白消耗CPU的資源,在自旋次數到了之后,將會被掛起。
在jdk1.4的時候,自旋鎖默認關閉;jdk1.6之后,自旋鎖默認開啟,默認自旋10次,當然也可以使用PreBlockSpin來修改自旋次數。
自旋鎖的痛點在于:無法在不同場景中,確定出一個可靠的自旋次數。因此,衍生出來適應性自旋鎖。
適應性自旋鎖
在適應性自旋鎖中,自旋的次數不再固定,一般由之前自旋的次數和鎖持有者的狀態決定。
如果在一個鎖對象上,之前的線程都能通過自旋來獲取到鎖,并且沒有超過自旋次數,那么虛擬機認為,通過自旋獲取到鎖的概率很大,下一次會增加自旋的次數。相反的,如果之前很少有線程通過自旋獲取到鎖,那么虛擬機會減少自旋的次數,減少到一定次數后,甚至會直接放棄自旋,升級為重量級鎖。
可以看出,適應性自旋鎖十分機智。
鎖消除
從字面意思上可以看出,這是一種直接去除鎖的方法,簡單粗暴。
對于那些根本不可能存在鎖競爭卻又包含鎖的情況,虛擬機會直接消除這個鎖,避免無意義的鎖請求。比如我在純單線程中對某個方法或者變量加鎖,或者調用內部實現有鎖的對象(Vector、StringBuffer與HashTable等),虛擬機會直接消除毫無意義的加鎖。
鎖粗化
在上一文中【多線程】淺說Synchronized,我們談到了synchronized的應用-雙重檢驗鎖的優化過程,強調將加鎖的范圍盡量限制得小一些,直到存在鎖競爭的實際區域才加鎖,這樣程序運行更加高效。
但是,如果存在這樣的一種情況:反復的對同一個對象執行加鎖解鎖的操作,也會導致CPU資源的過度消耗。
鎖粗化,就是將反復的加鎖操作粗化成一個范圍更大的鎖,這樣加鎖只有一次。
例如,在循環內部,調用StringBuffer的append操作(關于StringBuffer,可以參考我的另外一篇文章【JAVA】String、StringBuilder、StringBuffer三者的區別),每次append都需要加鎖,虛擬機檢測到這種情況后,首先會對append脫鎖,然后進行鎖粗化,將鎖的范圍擴大到循環外部。
鎖的狀態
鎖的狀態有以下幾種:
- 無鎖狀態
- 偏向鎖狀態
- 輕量級鎖狀態
- 重量級鎖狀態
其中,無鎖狀態對應于鎖消除,Monitor對應于重量級鎖,也就是1.6之前的synchronized。
偏向鎖
偏向鎖的核心要義就體現在“偏”字上,這個鎖偏向第一個獲取到它的線程。
在大部分情況下,不存在激烈的鎖競爭,總是由同一個線程獲取到該鎖。那么為了減少同一個線程獲取鎖帶來的開銷,就引入了偏向鎖。
如果一個線程不斷的獲取到了鎖,那么該鎖就進入偏向鎖狀態。當這個線程再次請求鎖時,無需做任何同步操作,直接獲取到鎖。
當然,偏向鎖適用于基本無鎖競爭的情況,當鎖競爭激烈時,偏向鎖就失去了作用,會升級為輕量級鎖。
輕量級鎖
在偏向鎖的狀態下,此時又出現了一個線程,與偏向線程競爭該鎖,此時該鎖會升級為輕量級鎖。
舉個例子,比如創建一個線程1執行同步print()方法打印奇數,這時候的鎖狀態為偏向鎖。此時,再創建一個線程2同樣執行同步print()方法打印偶數,偏向鎖就會升級為輕量級鎖。線程1打印某個奇數時,線程2并沒有被掛起,而是處于一種自旋狀態,這種自旋效率很高。可是,當我再創建100個線程時,同樣執行同步print()方法,自旋的效率將會變得十分低下,此時輕量級鎖會升級為重量級鎖,即使用Monitor來進行同步。
鎖的升級
無鎖、偏向鎖、輕量級鎖與重量級鎖,會隨著鎖競爭的升級而升級。
從一開始的偏向鎖,產生鎖競爭后,升級為輕量級鎖,自旋失敗后,升級為重量級鎖,一般來說,鎖的升級是單向的。