今天小編給大家分享一下Java Synchronized鎖升級原理及過程源碼分析的相關(guān)知識點,內(nèi)容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
公司主營業(yè)務(wù):成都網(wǎng)站建設(shè)、網(wǎng)站設(shè)計、移動網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。創(chuàng)新互聯(lián)是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團隊。公司秉承以“開放、自由、嚴謹、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團隊有機會用頭腦與智慧不斷的給客戶帶來驚喜。創(chuàng)新互聯(lián)推出霍林郭勒免費做網(wǎng)站回饋大家。
在正式談synchronized的原理之前我們先談一下自旋鎖,因為在synchronized的優(yōu)化當中自旋鎖發(fā)揮了很大的作用。而需要了解自旋鎖,我們首先需要了解什么是原子性。
所謂原子性簡單說來就是一個一個操作要么不做要么全做,全做的意思就是在操作的過程當中不能夠被中斷,比如說對變量data
進行加一操作,有以下三個步驟:
將data
從內(nèi)存加載到寄存器。
將data
這個值加一。
將得到的結(jié)果寫回內(nèi)存。
原子性就表示一個線程在進行加一操作的時候,不能夠被其他線程中斷,只有這個線程執(zhí)行完這三個過程的時候其他線程才能夠操作數(shù)據(jù)data
。
我們現(xiàn)在用代碼體驗一下,在Java當中我們可以使用AtomicInteger
進行對整型數(shù)據(jù)的原子操作:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 將數(shù)據(jù)初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 對數(shù)據(jù) data 進行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 對數(shù)據(jù) data 進行原子加1操作 } }); // 啟動兩個線程 t1.start(); t2.start(); // 等待兩個線程執(zhí)行完成 t1.join(); t2.join(); // 打印最終的結(jié)果 System.out.println(data); // 200000 } }
從上面的代碼分析可以知道,如果是一般的整型變量如果兩個線程同時進行操作的時候,最終的結(jié)果是會小于200000。
我們現(xiàn)在來模擬一下一般的整型變量出現(xiàn)問題的過程:
主內(nèi)存data
的初始值等于0,兩個線程得到的data
初始值都等于0。
現(xiàn)在線程一將data
加一,然后線程一將data
的值同步回主內(nèi)存,整個內(nèi)存的數(shù)據(jù)變化如下:
現(xiàn)在線程二data
加一,然后將data
的值同步回主內(nèi)存(將原來主內(nèi)存的值覆蓋掉了):
我們本來希望data
的值在經(jīng)過上面的變化之后變成2
,但是線程二覆蓋了我們的值,因此在多線程情況下,會使得我們最終的結(jié)果變小。
但是在上面的程序當中我們最終的輸出結(jié)果是等于20000的,這是因為給data
進行+1
的操作是原子的不可分的,在操作的過程當中其他線程是不能對data
進行操作的。這就是原子性帶來的優(yōu)勢。
事實上上面的+1
原子操作就是通過自旋鎖實現(xiàn)的,我們可以看一下AtomicInteger
的源代碼:
public final int addAndGet(int delta) { // 在 AtomicInteger 內(nèi)部有一個整型數(shù)據(jù) value 用于存儲具體的數(shù)值的 // 這個 valueOffset 表示這個數(shù)據(jù) value 在對象 this (也就是 AtomicInteger一個具體的對象) // 當中的內(nèi)存偏移地址 // delta 就是我們需要往 value 上加的值 在這里我們加上的是 1 return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
上面的代碼最終是調(diào)用UnSafe
類的方法進行實現(xiàn)的,我們再看一下他的源代碼:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); // 從對象 o 偏移地址為 offset 的位置取出數(shù)據(jù) value ,也就是前面提到的存儲整型數(shù)據(jù)的變量 } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
上面的代碼主要流程是不斷的從內(nèi)存當中取對象內(nèi)偏移地址為offset
的數(shù)據(jù),然后執(zhí)行語句!compareAndSwapInt(o, offset, v, v + delta)
這條語句的主要作用是:比較對象o
內(nèi)存偏移地址為offset
的數(shù)據(jù)是否等于v
,如果等于v
則將偏移地址為offset
的數(shù)據(jù)設(shè)置為v + delta
,如果這條語句執(zhí)行成功返回 true
否則返回false
,這就是我們常說的Java當中的CAS。
看到這里你應(yīng)該就發(fā)現(xiàn)了當上面的那條語句執(zhí)行不成功的話就會一直進行while循環(huán)操作,直到操作成功之后才退出while循環(huán),假如沒有操作成功就會一直“旋”在這里,像這種操作就是自旋,通過這種自旋方式所構(gòu)成的鎖就叫做自旋鎖。
在JVM當中,一個Java對象的內(nèi)存主要有三塊:
對象頭,對象頭包含兩部分數(shù)據(jù),分別是Mark word和類型指針(Kclass pointer)。
實例數(shù)據(jù),就是我們在類當中定義的各種數(shù)據(jù)。
對齊填充,JVM在實現(xiàn)的時候要求每一個對象所占有的內(nèi)存大小都需要是8字節(jié)的整數(shù)倍,如果一個對象的數(shù)據(jù)所占有的內(nèi)存大小不夠8字節(jié)的整數(shù)倍,那就需要進行填充,補齊到8字節(jié),比如說如果一個對象站60字節(jié),那么最終會填充到64字節(jié)。
而與我們要談到的synchronized鎖升級原理密切相關(guān)的是Mark word,這個字段主要是存儲對象運行時的數(shù)據(jù),比如說對象的Hashcode、GC的分代年齡、持有鎖的線程等等。而Kclass pointer主要是用于指向?qū)ο蟮念?,主要是表示這個對象是屬于哪一個類,主要是尋找類的元數(shù)據(jù)。
在32位Java虛擬機當中Mark word有4個字節(jié)一共32個比特位,其內(nèi)容如下:
我們在使用synchronized時,如果我們是將synchronized用在同步代碼塊,我們需要一個鎖對象。對于這個鎖對象來說一開始還沒有線程執(zhí)行到同步代碼塊時,這個4個字節(jié)的內(nèi)容如上圖所示,其中有25個比特用來存儲哈希值,4個比特用來存儲垃圾回收的分代年齡(如果不了解可以跳過),剩下三個比特其中第一個用來表示當前的鎖狀態(tài)是否為偏向鎖,最后的兩個比特表示當前的鎖是哪一種狀態(tài):
如果最后三個比特是:001,則說明鎖狀態(tài)是沒有鎖。
如果最后三個比特是:101,則說明鎖狀態(tài)是偏向鎖。
如果最后兩個比特是:00, 則說明鎖狀態(tài)是輕量級鎖。
如果最后兩個比特是:10, 則說明鎖狀態(tài)是重量級鎖。
而synchronized鎖升級的順序是:無????->偏向????->輕量級????->重量級????。
在Java當中有一個JVM參數(shù)用于設(shè)置在JVM啟動多少秒之后開啟偏向鎖(JDK6之后默認開啟偏向鎖,JVM默認啟動4秒之后開啟對象偏向鎖,這個延遲時間叫做偏向延遲,你可以通過下面的參數(shù)進行控制):
//設(shè)置偏向延遲時間 只有經(jīng)過這個時間只有對象鎖才會有偏向鎖這個狀態(tài) -XX:BiasedLockingStartupDelay=4 //禁止偏向鎖 -XX:-UseBiasedLocking //開啟偏向鎖 -XX:+UseBiasedLocking
我們可以用代碼驗證一下在無鎖狀態(tài)下,MarkWord的內(nèi)容是什么:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { synchronized (o) { System.out.println("synchronized代碼塊內(nèi)"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { System.out.println("等待4s前"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); TimeUnit.SECONDS.sleep(4); MarkWord markWord = new MarkWord(); System.out.println("等待4s后"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
上面代碼輸出結(jié)果,下面的紅框框住的表示是否是偏向鎖和鎖標志位(可能你會有疑問為什么是這個位置,不應(yīng)該是最后3個比特位表示鎖相關(guān)的狀態(tài)嗎,這個其實是數(shù)據(jù)表示的大小端問題,大家感興趣可以去查一下,在這你只需知道紅框三個比特就是用于表示是否為偏向鎖和鎖的標志位):
從上面的圖當中我們可以分析得知在偏向延遲的時間之前,對象鎖的狀態(tài)還不會有偏向鎖,因此對象頭中的Markword當中鎖狀態(tài)是01,同時偏向鎖狀態(tài)是0,表示這個時候是無鎖狀態(tài),但是在4秒之后偏向鎖的狀態(tài)已經(jīng)變成1了,因此當前的鎖狀態(tài)是偏向鎖,但是還沒有線程占有他,這種狀態(tài)也被稱作匿名偏向,因為在上面的代碼當中只有一個線程進入了synchronized同步代碼塊,因此可以使用偏向鎖,因此在synchronized代碼塊當中打印的對象的鎖狀態(tài)也是偏向鎖。
上面的代碼當中使用到了jol包,你需要在你的pom文件當中引入對應(yīng)的包:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
上圖當中我們顯示的結(jié)果是在64位機器下面顯示的結(jié)果,在64位機器當中在Java對象頭當中的MarkWord和Klcass Pointer內(nèi)存布局如下:
其中MarkWord占8個字節(jié),Kclass Pointer占4個字節(jié)。JVM在64位和32位機器上的MarkWord內(nèi)容基本一致,64位機器上和32位機器上的MarkWord內(nèi)容和表示意義是一樣的,因此最后三位的意義你可以參考32位JVM的MarkWord。
假如你寫的synchronized代碼塊沒有多個線程執(zhí)行,而只有一個線程執(zhí)行的時候這種鎖對程序性能的提高還是非常大的。他的具體做法是JVM會將對象頭當中的第三個用于表示是否為偏向鎖的比特位設(shè)置為1,同時會使用CAS操作將線程的ID記錄到Mark Word當中,如果操作成功就相當于獲得????了,那么下次這個線程想進入臨界區(qū)就只需要比較一下線程ID是否相同了,而不需要進行CAS或者加鎖這樣花費比較大的操作了,只需要進行一個簡單的比較即可,這種情況下加鎖的開銷非常小。
可能你會有一個疑問在無鎖的狀態(tài)下Mark Word存儲的是哈希值,而在偏向鎖的狀態(tài)下存儲的是線程的ID,那么之前存儲的Hash Code不就沒有了嘛!你可能會想沒有就沒有吧,再算一遍不就行了!事實上不是這樣,如果我們計算過哈希值之后我們需要盡量保持哈希值不變(但是這個在Java當中并沒有強制,因為在Java當中可以重寫hashCode方法),因此在Java當中為了能夠保持哈希值的不變性就會在第一次計算一致性哈希值(Mark Word里面存儲的是一致性哈希值,并不是指重寫的hashCode返回值,在Java當中可以通過 Object.hashCode()或者System.identityHashCode(Object)方法計算一致性哈希值)的時候就將計算出來的一致性哈希值存儲到Mark Word當中,下一次再有一致性哈希值的請求的時候就將存儲下來的一致性哈希值返回,這樣就可以保證每次計算的一致性哈希值相同。但是在變成偏向鎖的時候會使用線程ID覆蓋哈希值,因此當一個對象計算過一致性哈希值之后,他就再也不能進行偏向鎖狀態(tài),而且當一個對象正處于偏向鎖狀態(tài)的時候,收到了一致性哈希值的請求的時候,也就是調(diào)用上面提到的兩個方法,偏向鎖就會立馬膨脹為重量級鎖,然后將Mark Word 儲在重量級鎖里。
下面的代碼就是驗證當在偏向鎖的狀態(tài)調(diào)用System.identityHashCode
函數(shù)鎖的狀態(tài)就會升級為重量級鎖:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { System.out.println("System.identityHashCode(o) 函數(shù)之前"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.identityHashCode(o); System.out.println("System.identityHashCode(o) 函數(shù)之后"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { TimeUnit.SECONDS.sleep(5); MarkWord markWord = new MarkWord(); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); TimeUnit.SECONDS.sleep(2); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
輕量級鎖也是在JDK1.6加入的,當一個線程獲取偏向鎖的時候,有另外的線程加入鎖的競爭時,這個時候就會從偏向鎖升級為輕量級鎖。
在輕量級鎖的狀態(tài)時,虛擬機首先會在當前線程的棧幀當中建立一個鎖記錄(Lock Record),用于存儲對象MarkWord的拷貝,官方稱這個為Displaced Mark Word。然后虛擬機會使用CAS操作嘗試將對象的MarkWord指向棧中的Lock Record,如果操作成功說明這個線程獲取到了鎖,能夠進入同步代碼塊執(zhí)行,否則說明這個鎖對象已經(jīng)被其他線程占用了,線程就需要使用CAS不斷的進行獲取鎖的操作,當然你可能會有疑問,難道就讓線程一直死循環(huán)了嗎?這對CPU的花費那不是太大了嗎,確實是這樣的因此在CAS滿足一定條件的時候輕量級鎖就會升級為重量級鎖,具體過程在重量級鎖章節(jié)中分析。
當線程需要從同步代碼塊出來的時候,線程同樣的需要使用CAS將Displaced Mark Word替換回對象的MarkWord,如果替換成功,那么同步過程就完成了,如果替換失敗就說明有其他線程嘗試獲取該鎖,而且鎖已經(jīng)升級為重量級鎖,此前競爭鎖的線程已經(jīng)被掛起,因此線程在釋放鎖的同時還需要將掛起的線程喚醒。
所謂重量級鎖就是一種開銷最大的鎖機制,在這種情況下需要操作系統(tǒng)將沒有進入同步代碼塊的線程掛起,JVM(Linux操作系統(tǒng)下)底層是使用pthread_mutex_lock
、pthread_mutex_unlock
、pthread_cond_wait
、pthread_cond_signal
和pthread_cond_broadcast
這幾個庫函數(shù)實現(xiàn)的,而這些函數(shù)依賴于futex
系統(tǒng)調(diào)用,因此在使用重量級鎖的時候因為進行了系統(tǒng)調(diào)用,進程需要從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)將線程掛起,然后從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài),當解鎖的時候又需要從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)將線程喚醒,這一來二去的花費就比較大了(和CAS自旋鎖相比)。
在有兩個以上的線程競爭同一個輕量級鎖的情況下,輕量級鎖不再有效(輕量級鎖升級的一個條件),這個時候鎖為膨脹成重量級鎖,鎖的標志狀態(tài)變成10,MarkWord當中存儲的就是指向重量級鎖的指針,后面等待鎖的線程就會被掛起。
因為這個時候MarkWord當中存儲的已經(jīng)是指向重量級鎖的指針,因此在輕量級鎖的情況下進入到同步代碼塊在出同步代碼塊的時候使用CAS將Displaced Mark Word替換回對象的MarkWord的時候就會替換失敗,在前文已經(jīng)提到,在失敗的情況下,線程在釋放鎖的同時還需要將被掛起的線程喚醒。
以上就是“Java Synchronized鎖升級原理及過程源碼分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
網(wǎng)站名稱:Java?Synchronized鎖升級原理及過程源碼分析
標題鏈接:http://jinyejixie.com/article28/jjpgcp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)網(wǎng)站建設(shè)、外貿(mào)建站、微信小程序、網(wǎng)站設(shè)計、服務(wù)器托管、關(guān)鍵詞優(yōu)化
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)