java-synchronized原理


介紹

synchronized是一種獨占式的重量級鎖,在運行到同步方法或者同步代碼塊的時候,讓程序的運行級別由用戶態切換到內核態,把所有的線程掛起,通過操作系統的指令,去調度線程。這樣會頻繁出現程序運行狀態的切換,線程的掛起和喚醒,會消耗系統資源,為了提高效率,引入了偏向鎖、輕量級鎖、盡量讓多線程訪問公共資源的時候,不進行程序運行狀態的切換。

synchronized實現原理

synchronized是在jvm中實現,是基於進入和退出Monitor對象來實現方法和代碼塊的同步

同步代碼塊:

monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結束位置,JVM需要保證每一個monitorenter都有一個monitorexit與之相對應。任何對象都有一個monitor與之相關聯,當且一個monitor被持有之后,他將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,即嘗試獲取對象的鎖;

同步方法

synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,有一個ACC_SYNCHRONIZED標志,JVM就是通過該標志來判斷是否需要實現同步的,具體過程為:當線程執行該方法時,會先檢查該方法是否標志了ACC_SYNCHRONIZED,如果標志了,線程需要先獲取monitor,獲取成功后才能調用方法,方法執行完后再釋放monitor,在該線程調用方法期間,其他線程無法獲取同一個monitor對象。其實本質上和synchronized塊相同,只是同步方法是用一種隱式的方式來實現,而不是顯式地通過字節碼指令。

synchronized 作用

(1)確保線程互斥的訪問同步代碼
(2)保證共享變量的修改能夠及時可見
(3)有效解決重排序問題

Java中每一個對象都可以作為鎖,這是synchronized實現同步的基礎:

  1. 普通同步方法,鎖是當前實例對象
  2. 靜態同步方法,鎖是當前類的class對象
  3. 同步方法塊,鎖是括號里面的對象

自旋概念

互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的並發性能 帶來了很大的壓力。同時,虛擬機的開發團隊也注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復線程並不值得。如 果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓后面請求鎖的那個線程“稍等一會”,但不放棄處理器的執行時間,看看持有 鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只須讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。

--->自旋鎖在JDK 1.4.2中就已經引入,只不過默認是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK 1.6中就已經改為默認開啟了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的, 所以如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之如果鎖被占用的時間很長,那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作, 反而會帶來性能的浪費。因此自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次 數的默認值是10次,用戶可以使用參數-XX:PreBlockSpin來更改。

--->在JDK 1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象 上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間, 比如100個循環。另一方面,如果對於某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自 旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越准確,虛擬機就會變得越來越“聰明”了。

鎖削除

鎖削除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除。鎖削除的主要判定依據來源於逃逸分析的數 據支持,如果判斷到一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上數據對待, 認為它們是線程私有的,同步加鎖自然就無須進行。

變量是否逃逸,對於虛擬機來說需要使用數據流分析來確定,但是程序員自己應該是很清楚的,怎么會在明知道不存在數據爭用的 情況下要求同步呢?答案是有許多同步措施並不是程序員自己加入的,同步的代碼在Java程序中的普遍程度也許超過了大部分讀者的想象。比如:(只是說明概念,但實際情況並不一定如例子)在線程安全的環境中使用stringBuffer進行字符串拼加。則會在java文件編譯的時候,進行鎖銷除。

鎖粗化

我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小——只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快地拿到鎖。

大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(鎖粗化)到整個操作序列的外部。

鎖的狀態

鎖一共有四種狀態(由低到高的次序):無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態

鎖的等級只可以升級,不可以降級。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,為了讓線程獲得所得代價更低而引入了偏向鎖,當一個線程訪問同步代碼塊並獲取鎖時,會在線程的棧幀里創建lockRecord,在lockRecord里和鎖對象的MarkWord里存儲線程a的線程id.以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,說明是其他線程訪問這個對象,因為偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態,這時表明在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變為無鎖狀態,然后重新偏向新的線程,如果原來的線程依然存活,則檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖(偏向鎖就是這個時候升級為輕量級鎖的)。如果不存在使用了,則可以將對象回復成無鎖狀態,然后重新偏向。

輕量級鎖

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word復制到鎖記錄中,然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。如果,完成自旋策略還是發現線程沒有釋放鎖,或者讓別的線程占用,則線程試圖將輕量級鎖升級為重量級鎖。

輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。

重量級鎖

就是讓爭搶鎖的線程從用戶態轉換成內核態。讓cpu借助操作系統進行線程協調。

具體流程

每一個線程在准備獲取共享資源時:

第一步,檢查MarkWord里面是不是放的自己的ThreadId ,如果是,表示當前線程是處於 “偏向鎖”.跳過輕量級鎖直接執行同步體。

第二步,如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的線程根據MarkWord里面現有的ThreadId,通知之前線程暫停,之前線程將Markword的內容置為空。

第三步,兩個線程都把對象的HashCode復制到自己新建的用於存儲鎖的記錄空間,接着開始通過CAS操作,把共享對象的MarKword的內容修改為自己新建的記錄空間的地址的方式競爭MarkWord.

第四步,第三步中成功執行CAS的獲得資源,失敗的則進入自旋.

第五步,自旋的線程在自旋過程中,成功獲得資源(即之前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,如果自旋失敗

第六步,進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待之前線程執行完成並喚醒自己.

偏向鎖,輕量級鎖,重量級鎖對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景(只有一個線程進入臨界區)
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到索競爭的線程,使用自旋會消耗CPU 追求響應速度,同步塊執行速度非常快(多個線程交替進入臨界區)
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較慢(多個線程同時進入臨界區)

如果上面的沒看懂,可以先看看下面形象的例子:

打個比喻,假設你要回家上廁所,你要關上家里的大門,再關上自己房間的門,再關上房間廁所的門。如果你是一個人在家的話,那么其實只要關上家里的大門就好了,沒人會來跟你搶上廁所(或者偷窺你~),就不用關上房間門和廁所門了,這就是偏向鎖,只是你一個人的情況時才有用,即一個線程在獲取鎖的時候,重入的時候就不用做任何操作了,這不是很省事嘛。
那,如果家里有人,你就不能這么做了,他可能會跟你搶廁所呢,這就是偏向鎖膨脹成輕量級鎖。這就是多個線程交替獲取鎖的情況。那這個時候又要怎么做呢?比如說你哥上着廁所了,你也想上廁所,你猜你哥上廁所的時間不會太久,於是你就在廁所門口等一會(自旋),以前synchronized的方案就是讓你回房間躺着等(阻塞),可能回房間的時間都比你拉尿的時間長(掛起線程的時間比執行同步方法中的時間還要長的情況)。這就是輕量級鎖。
接着上面的故事,當你等的有點久了,你會覺得你哥可能這次要上很久了,所以你就回房間等了(阻塞),這時候就從輕量級鎖升級到重量級鎖了。


免責聲明!

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



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