synchronized的底层原理
synchronized使用方式
我們知道并發編程會產生各種問題的源頭是可見性,原子性,有序性。
而synchronized能同時保證可見性,原子性,有序性。所以我們在解決并發問題的時候經常用synchronized,當然還有很多其他工具,如volatile。但是volatile只能保證可見性,有序性,不能保證原子性。參見之前的文章volatile關鍵字——保證并發編程中的可見性、有序性
synchronized可以用在如下地方
修飾實例方法
public class SynchronizedDemo {public synchronized void methodOne() {} }修飾靜態方法
public class SynchronizedDemo {public static synchronized void methodTwo() {} }修飾代碼塊
public class SynchronizedDemo {public void methodThree() {// 對當前實例對象this加鎖synchronized (this) {}}public void methodFour() {// 對class對象加鎖synchronized (SynchronizedDemo.class) {}} }synchronized實現原理
Java對象組成
我們都知道對象是放在堆內存中的,對象大致可以分為三個部分,分別是對象頭,實例變量和填充字節
synchronized不論是修飾方法還是代碼塊,都是通過持有修飾對象的鎖來實現同步,那么synchronized鎖對象是存在哪里的呢?答案是存在鎖對象的對象頭Mark Word,來看一下Mark Word存儲了哪些內容?
由于對象頭的信息是與對象自身定義的數據沒有關系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設計成為一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象本身的狀態復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發生變化,變化狀態如下 (32位虛擬機):
其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化后新增加的,稍后我們會簡要分析。這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關聯。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的),省略部分屬性
注意這里的_entrylist和WaitSet,wait線程是在一個set里,這會不會notify隨機喚醒對應起來。
結合線程狀態解釋一下執行過程。
對于一個synchronized修飾的方法(代碼塊)來說:
由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的是指針),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因
synchronized如何獲取monitor對象?
synchronized修飾代碼塊
public class SyncCodeBlock {public int count = 0;public void addOne() {synchronized (this) {count++;}} } javac SyncCodeBlock.java javap -v SyncCodeBlock.class反編譯的字節碼如下
public void addOne();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter // 進入同步方法4: aload_05: dup6: getfield #2 // Field count:I9: iconst_110: iadd11: putfield #2 // Field count:I14: aload_115: monitorexit // 退出同步方法16: goto 2419: astore_220: aload_121: monitorexit // 退出同步方法22: aload_223: athrow24: returnException table:可以看到進入同步代碼塊,執行monitorenter指令,退出同步代碼塊,執行monitorexit指令,可以看到有2個monitorexit指令,第一個是正常退出執行的,第二個是當異常發生時執行的
synchronized修飾方法
public class SyncMethod {public int count = 0;public synchronized void addOne() {count++;} }反編譯的字節碼如下
public synchronized void addOne();descriptor: ()V// 方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field count:I5: iconst_16: iadd7: putfield #2 // Field count:I10: returnLineNumberTable:我們并沒有看到monitorenter和monitorexit指令,那是怎么來實現同步的呢?
可以看到方法被標識為ACC_SYNCHRONIZED,表明這是一個同步方法
synchronized鎖的升級
在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖和輕量級鎖,簡單介紹一下
synchronized鎖有四種狀態,無鎖,偏向鎖,輕量級鎖,重量級鎖,這幾個狀態會隨著競爭狀態逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態可以被重置為無鎖狀態
偏向鎖
為什么要引入偏向鎖?
因為經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖。
偏向鎖原理和升級過程
當線程1訪問代碼塊并獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因為偏向鎖不會主動釋放鎖,因此以后線程1再次獲取鎖的時候,需要比較當前線程的threadID和Java對象頭中的threadID是否一致,如果一致(還是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那么需要查看Java對象頭中記錄的線程1是否存活,如果線程1存活且仍需要持有這個鎖,那么暫停當前線程1,撤銷偏向鎖,升級為輕量級鎖。如果線程1沒有存活或者不再需要這個鎖,那么鎖對象被重置為無鎖狀態,其它線程(線程2)可以競爭將其設置為偏向鎖。
輕量級鎖
為什么要引入輕量級鎖?
輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個線程,讓它自旋這等待鎖釋放。
輕量級鎖原理和升級過程
線程1獲取輕量級鎖時會先把鎖對象的對象頭MarkWord復制一份到線程1的棧幀中創建的用于存儲鎖記錄的空間(稱為DisplacedMarkWord),然后使用CAS把對象頭中的內容替換為線程1存儲的鎖記錄(DisplacedMarkWord)的地址;
如果在線程1復制對象頭的同時(在線程1CAS之前),線程2也準備獲取鎖,復制了對象頭到線程2的鎖記錄空間中,但是在線程2CAS的時候,發現線程1已經把對象頭換了,線程2的CAS失敗,那么線程2就嘗試使用自旋鎖來等待線程1釋放鎖。 自旋鎖簡單來說就是讓線程2在循環中不斷CAS,但是如果自旋的時間太長也不行,因為自旋是要消耗CPU的,因此自旋的次數是有限制的,比如10次或者100次。如果自旋次數到了線程1還沒有釋放鎖,或者線程1還在執行,線程2還在自旋等待,這時又有一個線程3過來競爭這個鎖對象,那么這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止CPU空轉。
幾種鎖的優缺點
synchronized鎖的最佳實踐
-
錯誤的加鎖姿勢1
synchronized (new Object())
每次調用創建的是不同的鎖,相當于無鎖
-
錯誤的加鎖姿勢2
private Integer count; synchronized (count)String,Integer 都用了享元模式,即值在一定范圍內對象是同一個。所以看似是用了不同的對象,其實用的是同一個對象。會導致一個鎖被多個地方使用
-
正確的加鎖姿勢
// 普通對象鎖 private final Object lock = new Object(); // 靜態對象鎖 private static final Object lock = new Object();
最后總結一波,synchronized和ReentrantLock的異同
既然有了synchronized,為啥還要提供Lock接口呢?也許你會說Lock接口比synchronized性能高。在jdk1.5之前確實如此,但是在jdk1.6之后,兩者性能差不多了。
總結
以上是生活随笔為你收集整理的synchronized的底层原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySql数据库索引底层数据结构
- 下一篇: java代码从编译到加载执行的过程