您好,登錄后才能下訂單哦!
這篇文章給大家介紹怎么剖析volatile、synchronized實現原理,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
在java并發編程中volatile和synchronized都扮演著重要的角色。兩者都起到相同的作用:保證共享變量的線程可見性。與synchronized相比volatile可以看做是輕量級的synchronized,沒有線程的上下文切換和調試,性能比synchronized要好很多。但需要注意的是volatile變量在復合操作的時候并不能保證線程安全,相反sychronized能。下面從底層看一下volatile、synchronized到底是怎么實現的。
在介紹volatile前,我們先來簡單介紹一下java的內存模型
public int i = 1
假設對象有個屬性字段i,初始值為1,對象位于堆上。我們通常把堆看作是主內存,此時分別兩個兩個不同的線程訪問字段i。現代操作系統,每個線程都分配有單獨的處理器緩存,用這些處理器緩存去緩存一些數據,就可以不用再次訪問主內存去獲取相應的數據,這樣就可以提高效率。看下圖
這樣做可以提高效率,但同時也帶來的一個問題:修改數據時,各線程的數據不一致。
這樣做可以提高效率,但同時也帶來的一個問題:修改數據時,各線程的數據不一致。
所以這時候我們需要做到當線程1修改一個共享變量時,其它訪問該共享變量的線程能夠感知到變化。而這個功能volatile可以做到。我們來看下volatile到底是怎樣工作的。
volatile 只能用于修飾變量。代碼:
volatile public int i = 1;
當volatile變量i被賦值2時,這時線程1會做兩件事:
更新主內存。
向CPU總線發送一個修改信號。
這時監聽CPU總線的處理器會收到這個修改信號后,如果發現修改的數據自己緩存了,就把自己緩存的數據失效掉。這樣其它線程訪問到這段緩存時知道緩存數據失效了,需要從主內存中獲取。這樣所有線程中的共享變量i就達到了一致性。
所以volatile也可以看作線程間通信的一種廉價方式。
在多線程并發編程中synchronized一直扮演著元老級角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE 1.6對synchronized進行各種優化之后,有引起情況下它就并不那重了。下面來詳細介紹這方面的內容。
synchronized實現同步的基礎是:Java中的每個對象都可作為鎖。所以synchronized鎖的都對象,只不過不同形式下鎖的對象不一樣。
對于普通同步方法,鎖的是當前實例對象。
對于靜態同步方法,鎖的是當前類的Class對象。
對于同步方法塊,鎖是Synchronized括號里配置的對象。
當一個線程試圖訪問同步代時,它必須先獲得鎖,才能執行代碼邏輯,退出的時候又必須釋放鎖。那鎖到底是怎么實現的呢?
在JVM規范中規定了synchronized是通過Monitor對象來實現方法和代碼塊的同步,但兩者實現細節有點一不樣。代碼塊同步是使用monitorenter和monitorexit指令,方法同步是使用另外一種方法實現,細節JVM規范并沒有詳細說明。但是,方法的同步同樣可以使用這兩指令來實現。
monitorenter指令是編譯后插入到同步代碼塊的開始位置,而monitorexit指令是插入到方法結束處和異常處。JVM保證了每個monitorenter都有對應的monitorexit。任何一個對象都有一個monitor與之關聯,當且一個monitor被持有后,對象將處于鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象對應monitor的所有權,即嘗試獲得對象的鎖。
那我們一直口口聲聲說的鎖,它到底在哪里呢?遠近天邊,近在眼前。上面提到對象可以作為鎖,其實鎖就在對象里面,準確來說是對象頭的Mark World結構中。關于對象頭的結構具體可參考:林林:聊聊java對象內存布局
這里簡單描述一下:
對象頭分為兩部分:Mark Word 與 Class Pointer(類型指針)。
Mark Word存儲了對象的hashCode、GC信息、鎖信息三部分,Class Pointer存儲了指向類對象信息的指針。在32位JVM上對象頭占用的大小是8字節,64位JVM則是16字節,兩種類型的Mark Word 和 Class Pointer各占一半空間大小。
下面的圖是32位JVM上面的對象頭展示。前面25bit是hashCode, 4bit是GC信息,后面兩位分別是偏向鎖標志與鎖狀態標志。Mark Word在不同的級別鎖時,存儲內容會發生變化。
上面提到Java SE 1.6對synchronized進行各種優化,這優化指的是什么呢。指的是synchronized不一定就是重量級鎖,它根據鎖的重量級分成了三種,由低到高:偏向鎖、輕量級鎖、重量級鎖。上面圖就展示了每種鎖在Mark Word的存儲內容。下面來介紹每種鎖的應用場景和升級過程。
偏向鎖:經過研究發現,在多線程競爭不激烈的環境或業務中,一個鎖總是由同一個線程多次獲得。這樣的話就沒有必要用生成重量級鎖和重復加鎖了,因為這樣代價會很高。所以這時可以通過引入偏向鎖來解決這代價過高的問題。具體做法是當一個線程嘗試去獲取鎖時,在對象頭和棧幀的鎖記錄里存儲指向當前線程的偏向鎖(CAS 操作),同時設置偏向鎖標志位為1(CAS 操作)。之后線程再對同一對象加鎖,只需要簡單測試一下對象頭里面是否存儲著指向當前線程的偏向鎖就可以了,不需要真正執行加鎖操作。這時其它線程來嘗試獲取鎖時,CAS操作是獲取不了鎖的,這時只能等待原先的線程把鎖撤銷了,才能競爭鎖。偏向鎖的撤銷需要等到全局安全點才能撤銷,這就意味著其它線程可能要等很長時間。所以競爭激烈的情況偏向鎖就不是很適用。這時應該升級為輕量級鎖。
輕量級鎖:線程在執行同步塊之前,JVM會先在當前線程的的棧幀中創建用于存儲鎖記錄的空間,并將對象頭中的Mark World復制到線程棧幀的鎖記錄空間里面。然后呢,用CAS操作把對象頭的Mark World替換為指向鎖記錄空間的指針。成功就表示獲得鎖了,失敗就暫時自旋一下,等待其它線程解鎖。如果自旋到了時間發現還不能獲得鎖,這時只有兩種情況:(1)競爭超激烈 (2)同步代碼執行時間太長。這時候如果還自旋是很不劃算的,因為不但不能快速獲取鎖,還會白白浪費了CPU。這種情景輕量級鎖就不合適了還不如升級為重量級鎖。
重量級鎖:所謂的重量級鎖,其實就是最原始和最開始java實現的阻塞鎖。在JVM中又叫對象監視器。這時鎖對象的對象頭字段指向的是一個互斥量,所有線程競爭重量級鎖,競爭失敗的線程進入阻塞狀態(操作系統層面),并且在鎖對象的一個等待池中等待被喚醒,被喚醒后的線程再次去競爭鎖資源。
所以偏向鎖、輕量級鎖、重量級鎖是適用于不同的競爭環境。
關于怎么剖析volatile、synchronized實現原理就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。