先綜述個結論:
一般說的synchronized用來做多線程同步功能,其實synchronized只是提供多線程互斥,而對象的wait()和notify()方法才提供線程的同步功能。
一般說synchronized是加鎖,或者說是加對象鎖,其實對象鎖只是synchronized在實現鎖機制中的一種鎖(重量鎖,用這種方式互斥線程開銷大所以叫重量鎖,或者叫對象monitor),而synchronized的鎖機制會根據線程競爭情況在運行會有偏向鎖、輕量鎖、對象鎖,自旋鎖(或自適應自旋鎖)等,總之,synchronized可以認為是一個幾種鎖過程的封裝。
原理
通常說的synchronized在方法或塊上加鎖,這里的鎖就是對象鎖(當然也可以在類上面),或者叫重量鎖,在JVM中又叫對象監視器(Monitor),就是對象來監視線程的互斥。
先來回顧一下對象在堆里的邏輯結構:
對象頭里的結構大致如此:
其中Tag的2bit用來顯示鎖類型。通常我們說synchronized的對象鎖,就是這里Tag=10時的monitor對象,這里的Monitor address就是這個monitor對象(就是重量鎖)的地址。
當多個線程同時請求synchronized方法或塊時,monitor會設置幾個虛擬邏輯數據結構來管理這些多線程。下圖是簡化了的管理結構。
新請求的線程會首先被加入到線程排隊隊列中,線程阻塞,當某個擁有鎖的線程unlock之后,則排隊隊列里的線程競爭上崗(synchronized是不公平競爭鎖,下面還會講到)。如果運行的線程調用對象的wait()后就釋放鎖并進入wait線程集合那邊,當調用對象的notify()或notifyall()后,wait線程就到排隊那邊。這是大致的邏輯。
同時再看看線程的狀態圖
Blocked就是阻塞狀態。
wait()和sleep()最大的不同在于wait()會釋放對象鎖,而sleep()不會!wait、sleep、yield區別如下:
似乎講到這里,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是一個典型的非公平鎖方案,而且沒法做成公平鎖。