java程式設計師面試必備:synchronized、鎖、多線程同步的原理

先綜述個結論:

一般說的synchronized用來做多線程同步功能,其實synchronized只是提供多線程互斥,而對象的wait()和notify()方法才提供線程的同步功能。

一般說synchronized是加鎖,或者說是加對象鎖,其實對象鎖只是synchronized在實現鎖機制中的一種鎖(重量鎖,用這種方式互斥線程開銷大所以叫重量鎖,或者叫對象monitor),而synchronized的鎖機制會根據線程競爭情況在運行會有偏向鎖、輕量鎖、對象鎖,自旋鎖(或自適應自旋鎖)等,總之,synchronized可以認為是一個幾種鎖過程的封裝。


原理

通常說的synchronized在方法或塊上加鎖,這裡的鎖就是對象鎖(當然也可以在類上面),或者叫重量鎖,在JVM中又叫對象監視器(Monitor),就是對象來監視線程的互斥。

先來回顧一下對象在堆裡的邏輯結構:

java程序員面試必備:synchronized、鎖、多線程同步的原理

對象在內存中的結構看這裡》》

對象頭裡的結構大致如此:

java程序員面試必備:synchronized、鎖、多線程同步的原理

其中Tag的2bit用來顯示鎖類型。通常我們說synchronized的對象鎖,就是這裡Tag=10時的monitor對象,這裡的Monitor address就是這個monitor對象(就是重量鎖)的地址。

當多個線程同時請求synchronized方法或塊時,monitor會設置幾個虛擬邏輯數據結構來管理這些多線程。下圖是簡化了的管理結構。

java程序員面試必備:synchronized、鎖、多線程同步的原理

新請求的線程會首先被加入到線程排隊隊列中,線程阻塞,當某個擁有鎖的線程unlock之後,則排隊隊列裡的線程競爭上崗(synchronized是不公平競爭鎖,下面還會講到)。如果運行的線程調用對象的wait()後就釋放鎖並進入wait線程集合那邊,當調用對象的notify()或notifyall()後,wait線程就到排隊那邊。這是大致的邏輯。

同時再看看線程的狀態圖

java程序員面試必備:synchronized、鎖、多線程同步的原理

Blocked就是阻塞狀態。

wait()和sleep()最大的不同在於wait()會釋放對象鎖,而sleep()不會!wait、sleep、yield區別如下:

java程序員面試必備:synchronized、鎖、多線程同步的原理

似乎講到這裡,synchronized鎖和wait()、notify()來實現多線程同步就完成了。

但是,自旋鎖或自適應自旋鎖:

因為線程阻塞後進入排隊隊列和喚醒都需要CPU從用戶態轉為核心態,尤其頻繁的阻塞和喚醒對CPU來說是負荷很重的工作。同時統計發現,很多對象鎖的鎖定狀態只會持續很短的一段時間,例如一個線程切換週期,這樣的話在很短的時間內阻塞線程又很快喚醒線程顯然不值得,所以引入了自旋鎖概念。

所謂“自旋”,就monitor並不把線程阻塞放入排隊隊列,而是去執行一個無意義的循環,循環結束後看看是否鎖已釋放並直接進行競爭上崗步驟,如果競爭不到繼續自旋循環,循環過程中線程的狀態一直處於running狀態。明顯自旋鎖使得synchronized的對象鎖方式在線程之間引入了不公平。但是這樣可以保證大吞吐率和執行效率。

不過雖然自旋鎖方式省去了阻塞線程的時間和空間(隊列的維護等)開銷,但是長時間自旋也是很低效的。所以自旋的次數一般控制在一個範圍內,例如10,50等,在超出這個範圍後,線程就進入排隊隊列。

自適應自旋鎖,就是自旋的次數是通過JVM在運行時收集的統計信息,動態調整自旋鎖的自旋次數上界。

講到這裡似乎synchronized鎖的過程更加豐滿了。

不過synchronized在運行過程中不是一下子就到對象鎖這個級別的,它根據線程競爭情況會經過幾次升級變化。這裡就出現了另外幾種鎖。

輕量鎖和偏向鎖

當多線程環境進入synchronized區域的線程沒競爭時,JVM並不會馬上創建對象鎖,而是用輕量鎖或偏向鎖。

不過需要明確的是,輕量鎖和偏向鎖,都不能代替重量鎖,只不過是在沒有多線程競爭時,沒必要用重量鎖而無畏的消耗資源。但是一旦出現了多線程競爭時,synchronized區域的輕量鎖或偏向鎖都會立即升級為重量鎖。

輕量鎖或偏向鎖使用的條件是進入synchronized區域時沒有其他任何其他線程在使用。

這時線程t訪問對象的synchronized區域時,對象頭的標誌位Tag狀態為01,以及還有1位的偏向信息用於記錄這個對象是否可用偏向鎖。然後t在對象上申請輕量鎖時,若偏向信息為0,表明當前對象還未加鎖,或加過偏向鎖(加過,注意是加過偏向鎖的對象只能被同樣的線程加鎖,如果不同的線程想要獲取鎖,需要先將偏向鎖升級為輕量鎖,稍後會講到),在判斷對當前對象確實沒有被任何其他線程鎖住後,即可以在該對象上加輕量鎖。

加輕量鎖的過程很簡單:在當前線程的棧幀(stack frame)中生成一個鎖記錄(lock record),這個鎖記錄比前面說的那個對象鎖(管理線程隊列的monitor)簡單多了,它只是對象頭的一個拷貝。然後把對象頭裡的tag改成00,並把這個棧幀裡的lock record地址放入對象頭裡。若操作成功,那就完成了輕量鎖操作。如果不成功,說明有線程在競爭,則需要在當前對象上生成重量鎖來進行多線程同步,然後將Tag狀態改為10,並生成Monitor對象(重量鎖對象),對象頭裡也會放入Monitor對象的地址。最後將當前線程t排隊隊列中。

輕量鎖的解鎖過程也很簡單就是把棧幀裡剛才的那個lock record拷貝到對象頭裡,若替換成功,則解鎖完成,若替換不成功,表示在當前線程持有鎖的這段時間內,其他線程也競爭過鎖,並且發生了鎖升級為重量鎖,這時需要去Monitor的等待隊列中喚醒一個線程去重新競爭鎖。

偏向鎖是比輕量鎖還輕量的鎖機制。當synchronized區域長期都由同一個線程加鎖、解鎖時,jvm就用偏向鎖來做,它的加鎖解鎖比輕量鎖操作起來指令更加簡化。不過一旦有其他線程使用synchronized區域,即使沒有線程間競爭,也會把偏向鎖升級為輕量鎖,當然如果發生線程競爭就再升級為對象鎖。

鎖的公平與不公平:公平鎖是指線程獲得鎖的順序按照fifo的原則,先排隊的先得。非公平鎖指每個線程都先要競爭鎖,不管排隊先後,所以後到的線程有可能無需進入等待隊列直接競爭到鎖。非公平鎖雖然可能導致某些線程飢餓,但是鎖的吞吐率是公平鎖好幾倍,synchronized是一個典型的非公平鎖方案,而且沒法做成公平鎖。


分享到:


相關文章: