java多線程synchronized底層實現


一直想把這個特別重要的關鍵詞的底層實現搞明白。(當然現在也沒有完全明白,如果有錯誤以后修改這篇文章)

 

首先,這個關鍵詞synchronize可以說是個語法糖,它的具體用法網上很多博客都講的比較明了了。

簡而言之就是對一個對象“加鎖”。首先,找個地方的對象不一定是堆里面的類的實例對象,也有可能是方法區的類對象。其次,這個關鍵詞修飾的代碼塊的加鎖過程有兩個,進入的時候嘗試獲得鎖(java字節碼 monitorenter),退出時釋放鎖(java字節碼monitorexit)。這兩個操作的再下一層是基於mutex lock的lock()和unluck()。

這兩個函數的具體實現由操作系統提供。lock()操作是“獲得鎖”“上鎖”“進入臨界區”,等等,不同的地方描述不一致。它的具體過程是:查看一個信號量(由這個鎖持有),看當前能否獲得鎖,如果能直接獲得,並且修改這個信號量的值(比如把1改成0)。如果不能,就把這個索取鎖的線程自己加入一個隊列,這隊列專門放“困”在這個對象(這個鎖)上的線程,接着阻塞這個線程自己。unlock()操作是“釋放鎖”“解鎖”“離開臨界區”。他可以直接修改信號量的值。同時他看是否有進程“困”在這個對象(鎖)上。如果有,喚醒它放入就緒隊列。(信號量的具體實現各不相同,記錄型信號量可以更方便理解這個過程)。當然這些基礎操作也是原子性的。

 

這個地方還有一個很重要的點,鎖,線程,對象這3個東西到底怎么聯系到一起的。而要講明白這個,又不得不講一下鎖不一定是重量級的由操作系統提供的“互斥鎖”。還有一種鎖:輕量級鎖。這種鎖是一種運行時優化,如果用synchronize修飾的代碼塊沒有發生並發行為就可以直接用這種“鎖”。

 

一開始,要明白Mark Word。這是每一個對象的對象頭中有的一個32bit或者64bit(由JVM確定)的一個區域(叫Mark Word,對象頭還有一個和它一樣大的區域保存了一個指針指向方法區的類對象)。保存了對象在運行過程中的一些數據,比如哈希碼,GC分代年齡,上鎖標志位等等。它的存在是必要的,因為確實有些運行時信息要通過這種形式保留。一個線程根據java字節碼找到上鎖的對象,查看上鎖標志位,看看是否已經被別的線程獲得鎖,如果沒有,就在這個線程的棧幀中建立一個鎖記錄(Lock Record,有的地方叫Monitor Record),保存這個Mark Word的一份拷貝,接着用一個Owner指針指向這個對象。對象則直接把Mark Word改成一個指針指向這個鎖記錄。(記得前面提到Mark Word的大小是剛好和當前操作系統指針的大小一樣,所以可以直接改而不需要補位等操作)

 

但是這個地方一開始我很不理解,為什么要這么繞呢?既然鎖在對象上面,為什么不直接在對象頭保留一個空間,記錄或者這個“鎖”的線程呢,比如用線程的ID或者內部標識符。每次線程進入臨界區,訪問這個對象的時候直接去對象頭看這個值是不是自己這個線程,如果不是就阻塞自己。

原因是:對象頭是很珍貴的,因為每一個對象都有,它雖然有必要但是它的內容又確實不是對象本身真正的內容。也就是說要想盡一切辦法縮小它的大小否則效率很低(試想你開辟的空間有一大半都儲存了一些雜七雜八的信息)。相比這個對象頭,運行時的程序棧可謂是非常廣闊的空間。一個珍惜這點空間一個無所謂這點空間。這樣就剛好通過“復用”來實現儲存空間的優化。把本來需要額外增加的空間直接用Mark Word儲存而它本來的值丟給線程的棧。這樣就一舉兩得,首先儲存了持有這個對象的鎖的線程,同時也沒有弄丟Mark Word(反正我用了指針指向這個線程也不怕找不到)。而線程則再用一個指針Owner指向這個對象。很完美。

 

這個是輕量級鎖的做法,如果不是輕量級呢?其實JVM的優化策略保證了一開始都把他當做輕量級來處理(JVM的優化策略有自旋鎖,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等)。這個地方也要解釋2點,第一,好處都有啥,第二,為什么能這么做。第三,為什么要這么做。

首先,如果直接用操作系統提供的Mutex Lock互斥鎖的話,會使用操作系統調用,從用戶態轉為核心態,開銷很大。用這種方式(輕量級鎖)則只是一個CAS操作(要保證其原子性)的花費。第二,馬上下面講到如果輕量級鎖沒用了(也就是發生了競爭別的線程試圖拿這個鎖),它可以直接“膨脹”成一個重量級鎖(Mutex Lock)。第三,現實情況是很多加了synchronize修飾的代碼其實在實際運行過程中並沒有發生競爭的情況,這么做在運行時直接減少了很多開銷。

然后,什么情況下會從一個輕量級膨脹成一個重量級的Mutex Lock呢?其實jvm這部分的優化是這樣的,一開始先“認為”這次加鎖和大多數情況一樣並沒有發生競爭,於是先“機智”地用輕量級,這個時候如果發生競爭,也就是有別的線程嘗試獲得鎖,就“膨脹”為一個重量級。再具體一點就是別的線程調用Lock(),發現當前這個對象的對象頭的標志位是“加了輕量級鎖的”。它再去看Owner,如果是自己就是一個“重入”。如果不是就說明發生了競爭,接着進行“膨脹”。這是第一種可能,也就是一個線程先在跑,后一個加入發生膨脹。其實還有第二種可能,發生在前一個線程剛放鎖的時候,這個時候所有線程都認為沒鎖,同時通過CAS競爭,有一個會成功,其他的會失敗,於是也進行膨脹。

 

接着來講膨脹的過程。第一,改變對象鎖標志的狀態值,把Mark Word中保存的指針指向Mutex Lock(當然Mark Word的內容還是不能丟)我看的是深入理解Java虛擬機這本書,再加上一些網絡上的博客。都沒把這個部分講明白,這個地方我“”一下:調用操作系統互斥鎖,生成一個互斥鎖並且把Mark Word的值指向它。同時讓這個互斥鎖或者別的什么數據結構和方式保存Mark Word原本的運行時信息。這個地方的Mutex Lock可能就是真真正正的“重量級”鎖了,它的具體實現我估計和記錄型信號量的PV原語操作差不多,同時還要保留一些標志位儲存Owner,重入個數,等待隊列的元素個數,等待隊列指針,如果當前釋放鎖是否有線程需要喚醒等信息。

 

以上內容能夠保證正確的是Mark Word指向一個Mutex Lock。這之后的過程就是再有線程調用Lock()嘗試訪問臨界區,發先對象頭指向一個鎖,再進入鎖發現已經上了鎖並且自己不是Owner,於是就阻塞自己。出臨界區的時候去喚醒別的阻塞線程。這都沒啥說的了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM