本篇內(nèi)容主要講解“Synchronized的原理介紹”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Synchronized的原理介紹”吧!
創(chuàng)新互聯(lián)建站是一家專業(yè)提供樂業(yè)企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、網(wǎng)站設(shè)計、H5開發(fā)、小程序制作等業(yè)務(wù)。10年已為樂業(yè)眾多企業(yè)、政府機構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)的建站公司優(yōu)惠進行中。
CAS全稱:CompareAndSwap,故名思意:比較并交換。他的主要思想就是:**我需要對一個值進行修改,我不會直接修改,而是將當前我認為的值和要修改的值傳入,如果此時內(nèi)存中的確為我認為的值,那么就進行修改,否則修改失敗。**他的思想是一種樂觀鎖的思想。
一張圖解釋他的工作流程:

知道了它的工作原理,我們來聽一個場景:現(xiàn)在有一個int類型的數(shù)字它等于1,存在三個線程需要對其進行自增操作。
一般來說,我們認為的操作步驟是這樣:線程從主內(nèi)存中讀取這個變量,到自己的工作空間中,然后執(zhí)行變量自增,然后回寫主內(nèi)存,但這樣在多線程狀態(tài)下會存在安全問題。而如果我們保證變量的安全性,常用的做法是ThreadLocal或者直接加鎖。(對ThreadLocal不了解的兄弟,看我這篇文章一文讀懂ThreadLocal設(shè)計思想)
這個時候我們思考一下,如果使用我們上面的CAS進行對值的修改,我們需要如何操作。
首先,我們需要將當前線程認為的值傳入,然后將想要修改的值傳入。如果此時內(nèi)存中的值和我們的期望值相等,進行修改,否則修改失敗。這樣是不是解決了一個多線程修改的問題,而且它沒有使用到操作系統(tǒng)提供的鎖。
上面的流程其實就是類AtomicInteger執(zhí)行自增操作的底層實現(xiàn),它保證了一個操作的原子性。我們來看一下源碼。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//從內(nèi)存中讀取最新值
var5 = this.getIntVolatile(var1, var2);
//修改
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}實現(xiàn)CAS使用到了Unsafe類,看它的名字就知道不安全,所以JDK不建議我們使用。對比我們上面多個線程執(zhí)行一個變量的修改流程,這個類的操作僅僅增加了一個自旋,它在不斷獲取內(nèi)存中的最新值,然后執(zhí)行自增操作。
可能有兄弟說了,那getIntVolatile和compareAndSwapInt操作如何保證原子性。
對于getIntVolatile來說,讀取內(nèi)存中的地址,本來就一部操作,原子性顯而易見。
對于compareAndSwapInt來說,它的原子性由CPU保證,通過一系列的CPU指令實現(xiàn),其C++底層是依賴于Atomic::cmpxchg_ptr實現(xiàn)的
到這里CAS講完了,不過其中還有一個ABA問題,有興趣可以去了解我的這篇文章多線程知識點小節(jié)。里面有詳細的講解。
我們通過CAS可以保證了操作的原子性,那么我們需要考慮一個東西,鎖是怎么實現(xiàn)的。對比生活中的case,我們通過一組密碼或者一把鑰匙實現(xiàn)了一把鎖,同樣在計算機中也通過一個鑰匙即synchronized代碼塊使用的鎖對象。
那其他線程如何判斷當前資源已經(jīng)被占有了呢?
在計算機中的實現(xiàn),往往是通過對一個變量的判斷來實現(xiàn),無鎖狀態(tài)為0,有鎖狀態(tài)為1等等來判斷這個資源是否被加鎖了,當一個線程釋放鎖時僅僅需要將這個變量值更改為0,代表無鎖。
我們僅僅需要保證在進行變量修改時的原子性即可,而剛剛的CAS剛好可以解決這個問題
至于那個鎖變量存儲在哪里這個問題,就是下面的內(nèi)容了,對象的內(nèi)存布局
各位兄弟們,應(yīng)該都清楚,我們創(chuàng)建的對象都是被存放到堆中的,最后我們獲得到的是一個對象的引用指針。那么有一個問題就會誕生了,JVM創(chuàng)建的對象的時候,開辟了一塊空間,那這個空間里都有什么東西?這個就是我們這個點的內(nèi)容。
先來結(jié)論:Java中存在兩種類型的對象,一種是普通對象,另一種是數(shù)組
對象內(nèi)存布局

我們來一個一個解釋其含義。
**白話版:**對象頭中包含又兩個字段,Mark Word主要存儲改對象的鎖信息,GC信息等等(鎖升級的實現(xiàn))。而其中的Klass Point代表的是一個類指針,它指向了方法區(qū)中類的定義和結(jié)構(gòu)信息。而Instance Data代表的就是類的成員變量。在我們剛剛學習Java基礎(chǔ)的時候,都聽過老師講過,對象的非靜態(tài)成員屬性都會被存放在堆中,這個就是對象的Instance Data。相對于對象而言,數(shù)組額外添加了一個數(shù)組長度的屬性
最后一個對其數(shù)據(jù)是什么?
我們拿一個場景來展示這個原因:**想像一下,你和女朋友周末打算出去玩,女朋友讓你給她帶上口紅,那么這個時候你僅僅會帶上口紅嘛?當然不是,而是將所有的必用品統(tǒng)統(tǒng)帶上,以防剛一出門就得回家拿東西!!!**這種行為叫啥?未雨綢繆,沒錯,暖男行為。還不懂?再來一個案例。你準備創(chuàng)業(yè)了,資金非常充足,你需要注冊一個域名,你僅僅注冊一個嘛?不,而是將所有相關(guān)的都注冊了,防止以后大價錢買域名。一個道理。
而對于CPU而言,它在進行計算處理數(shù)據(jù)的時候,不可能需要什么拿什么吧,那對其性能損耗非常嚴重。所以有一個協(xié)議,CPU在讀取數(shù)據(jù)的時候,不僅僅只拿需要的數(shù)據(jù),而是獲取一行的數(shù)據(jù),這就是緩存行,而一行是64個字節(jié)。
所以呢?通過這個特性可以玩一些詭異的花樣,比如下面的代碼。
public class CacheLine {
private volatile Long l1 , l2;
}我們給一個場景:兩個線程t1和t2分別操作l1和l2,那么當t1對l1做了修改以后,l2需不需要重新讀取主內(nèi)存種值。答案是一定,根據(jù)我們上面對于緩存行的理解,l1和l2必然位于同一個緩存行中,根據(jù)緩存一致性協(xié)議,當數(shù)據(jù)被修改以后,其他CPU需要重新重主內(nèi)存中讀取數(shù)據(jù)。這就引發(fā)了偽共享的問題
那么為什么對象頭要求會存在一個對其數(shù)據(jù)呢?
HotSpot虛擬機要求每一個對象的內(nèi)存大小必須保證為8字節(jié)的整數(shù)倍,所以對于不是8字節(jié)的進行了對其補充。其原因也是因為緩存行的原因
對象=對象頭+實例數(shù)據(jù)
我們在前面聊了一下,計算機中的鎖的實現(xiàn)思路和對象在內(nèi)存中的布局,接下來我們來聊一下它的具體鎖實現(xiàn),為對象加鎖使用的是對象內(nèi)存模型中的對象頭,通過對其鎖標志位和偏向鎖標志位的修改實現(xiàn)對資源的獨占即加鎖操作。接下來我們看一下它的內(nèi)存結(jié)構(gòu)圖。

上圖就是對象頭在內(nèi)存中的表現(xiàn)(64位),JVM通過對對象頭中的鎖標志位和偏向鎖位的修改實現(xiàn)“無鎖”。
對于無鎖這個概念來說,在1.6之前,即所有的對象,被創(chuàng)建了以后都處于無鎖狀態(tài),而在1.6之后,偏向鎖被開啟,對象在經(jīng)歷過幾秒的時候(4~5s)以后,自動升級為當前線程的偏向鎖。(無論經(jīng)沒經(jīng)過synchronized)。
我們來驗證一下,通過jol-core工具打印其內(nèi)存布局。注:該工具打印出來的數(shù)據(jù)信息是反的,即最后幾位在前面,通過下面的案例可以看到
場景:創(chuàng)建兩個對象,一個在剛開始的時候就創(chuàng)建,另一個在5秒之后創(chuàng)建,進行對比其內(nèi)存布局
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());//此時處于無鎖態(tài)
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
Object o = new Object();
System.out.println("偏向鎖開啟");
System.out.println(ClassLayout.parseInstance(o).toPrintable());//五秒以后偏向鎖開啟
我們可以看到,線程已開啟創(chuàng)建的對象處于無鎖態(tài),而在5秒以后創(chuàng)建的線程處于偏向鎖狀態(tài)。
同樣,當我們遇到synchronized塊的時候,也會自動升級為偏向鎖,而不是和操作系統(tǒng)申請鎖。
說完這個,提一嘴一個面試題吧。解釋一下什么是無鎖。
從對象內(nèi)存結(jié)構(gòu)的角度來說,是一個鎖標志位的體現(xiàn);從其語義來說,無鎖這個比較抽象了,因為在以前鎖的概念往往是與操作系統(tǒng)的鎖息息相關(guān),所以新出現(xiàn)的基于CAS的偏向鎖,輕量級鎖等等也被成為無鎖。而在synchronized升級的起點----無鎖。這個東西就比較難以解釋,只能說它沒加鎖。不過面試的過程中從對象內(nèi)存模型中理解可能會更加舒服一點。
在實際開發(fā)中,往往資源的競爭比較少,于是出現(xiàn)了偏向鎖,故名思意,當前資源偏向于該線程,認為將來的一切操作均來自于改線程。下面我們從對象的內(nèi)存布局下看看偏向鎖
對象頭描述:偏向鎖標志位通過CAS修改為1,并且存儲該線程的線程指針

當發(fā)生了鎖競爭,其實也不算鎖競爭,就是當這個資源被多個線程使用的時候,偏向鎖就會升級。
在升級的期間有一個點-----全局安全點,只有處在這個點的時候,才會撤銷偏向鎖。
全局安全點-----類似于CMS的stop the world,保證這個時候沒有任何線程在操作這個資源,這個時間點就叫做全局安全點。
可以通過XX:BiasedLockingStartupDelay=0 關(guān)閉偏向鎖的延遲,使其立即生效。
通過XX:-UseBiasedLocking=false 關(guān)閉偏向鎖。
在聊輕量級鎖的時候,我們需要搞明白這幾個問題。什么是輕量級鎖,什么重量級鎖?,為什么就重量了,為什么就輕量了?
輕量級和重量級的標準是依靠于操作系統(tǒng)作為標準判斷的,在進行操作的時候你有沒有調(diào)用過操作系統(tǒng)的鎖資源,如果有就是重量級,如果沒有就是輕量級
接下來我們看一下輕量級鎖的實現(xiàn)。
線程獲取鎖,判斷當前線程是否處于無鎖或者偏向鎖的狀態(tài),如果是,通過CAS復制當前對象的對象頭到Lock Recoder放置到當前棧幀中(對于JVM內(nèi)存模型不清楚的兄弟,看這里入門JVM看這一篇就夠了
通過CAS將當前對象的對象頭設(shè)置為棧幀中的Lock Recoder,并且將鎖標志位設(shè)置為00
如果修改失敗,則判斷當前棧幀中的線程是否為自己,如果是自己直接獲取鎖,如果不是升級為重量級鎖,后面的線程阻塞
我們在上面提到了一個Lock Recoder,這個東東是用來保存當前對象的對象頭中的數(shù)據(jù)的,并且此時在該對象的對象頭中保存的數(shù)據(jù)成為了當前Lock Recoder的指針

我們看一個代碼模擬案例,
public class QingLock {
public static void main(String[] args) {
try {
//睡覺5秒,開啟偏向鎖,可以使用JVM參數(shù)
TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
A o = new A();
//讓線程交替執(zhí)行
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(()->{
o.test();
countDownLatch.countDown();
},"1").start();
new Thread(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
o.test();
},"2").start();
}
}
class A{
private Object object = new Object();
public void test(){
System.out.println("為進入同步代碼塊*****");
System.out.println(ClassLayout.parseInstance(object).toPrintable());
System.out.println("進入同步代碼塊******");
for (int i = 0; i < 5; i++) {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
}運行結(jié)果為兩個線程交替前后

輕量級鎖強調(diào)的是線程交替使用資源,無論線程的個數(shù)有幾個,只要沒有同時使用就不會升級為重量級鎖
在上面的關(guān)于輕量級鎖加鎖步驟的講解中,如果線程CAS修改失敗,則判斷棧幀中的owner是不是自己,如果不是就失敗升級為重量級鎖,而在實際中,JDK加入了一種機制自旋鎖,即修改失敗以后不會立即升級而是進行自旋,在JDK1.6之前自旋次數(shù)為10次,而在1.6又做了優(yōu)化,改為了自適應(yīng)自旋鎖,由虛擬機判斷是否需要進行自旋,判斷原因有:當前線程之前是否獲取到過鎖,如果沒有,則認為獲取鎖的幾率不大,直接升級,如果有則進行自旋獲取鎖。
前面我們談到了無鎖-->偏向鎖-->輕量級鎖,現(xiàn)在最后我們來聊一下重量級鎖。
這個鎖在我們開發(fā)過程中很常見,線程搶占資源大部分都是同時的,所以synchronized會直接升級為重量級鎖。我們來代碼模擬看一下它的對象頭的狀況。
代碼模擬
public class WeightLock {
public static void main(String[] args) {
A a = new A();
for (int i = 0; i < 2; i++) {
new Thread(()->{
a.test();
},"線程"+ i).start();
}
}
}未進入代碼塊之前,兩者均為無鎖狀態(tài)

開始執(zhí)行循環(huán),進入代碼塊

在看一眼,對象頭鎖標志位

對比上圖,可以發(fā)現(xiàn),在線程競爭的時候鎖,已經(jīng)變?yōu)榱酥亓考夋i。接下來我們來看一下重量級鎖的實現(xiàn)
我們先從Java字節(jié)碼分析synchronzied的底層實現(xiàn),它的主要實現(xiàn)邏輯是依賴于一個monitor對象,當前線程執(zhí)行遇到monitorenter以后,給當前對象的一個屬性recursions加一(下面會詳細講解),當遇到monitorexit以后該屬性減一,代表釋放鎖。
代碼
Object o = new Object();
synchronized (o){
}匯編碼

上圖就是上面的四行代碼的匯編碼,我們可以看到synchronized的底層是兩個匯編指令
monitoreneter代表synchronized塊開始
monitorexit代表synchronized塊結(jié)束
有兄弟要說了為什么會有兩個monitorexit?這也是我曾經(jīng)遇到的一個面試題
第一個monitorexit代表了synchronized塊正常退出
第二個monitorexit代表了synchronized塊異常退出
很好理解,當在synchronized塊中出現(xiàn)了異常以后,不能當前線程一直拿著鎖不讓其他線程使用吧。所以出現(xiàn)了兩個monitorexit
同步代碼塊理解了,我們再來看一下同步方法。
代碼
public static void main(String[] args) {
}
public synchronized void test01(){
}匯編碼

我們可以看到,同步方法增加了一個ACC_SYNCHRONIZED標志,它會在同步方法執(zhí)行之前調(diào)用monitorenter,結(jié)束以后調(diào)用monitorexit指令。
在Java匯編碼的講解中,我們提到了兩個指令monitorenter和monitorexit,其實他們是來源于一個C++對象monitor,在Java中每創(chuàng)建一個對象的時候都會有一個monitor對象被隱式創(chuàng)建,他們和當前對象綁定,用于監(jiān)視當前對象的狀態(tài)。其實說綁定也不算正確,其實際流程為:線程本身維護了兩個MonitorList列表,分別為空閑(free)和已經(jīng)使用(used),當線程遇到同步代碼塊或者同步方法的時候,會從空閑列表中申請一個monitor使用,如果當先線程已經(jīng)沒有空閑的了,則直接從全局(JVM)獲取一個monitor使用
我們來看一下C++對這個對象的描述
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 重入次數(shù)
_object = NULL; //存儲該Monitor對象
_owner = NULL; //擁有該Monitor對象的對象
_WaitSet = NULL; //線程等待集合(Waiting)
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多線程競爭時的單向鏈表
FreeNext = NULL ;
_EntryList = NULL ; //阻塞鏈表(Block)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}線程加鎖模型

加鎖流程:
最新進入的線程會進入_cxp棧中,嘗試獲取鎖,如果當前線程獲得鎖就執(zhí)行代碼,如果沒有獲取到鎖則添加到EntryList阻塞隊列中
如果在執(zhí)行的過程的當前線程被掛起(wait)則被添加到WaitSet等待隊列中,等待被喚醒繼續(xù)執(zhí)行
當同步代碼塊執(zhí)行完畢以后,從_cxp或者EntryList中獲取一個線程執(zhí)行
monitorenter加鎖實現(xiàn)
CAS修改當前monitor對象的_owner為當前線程,如果修改成功,執(zhí)行操作;
如果修改失敗,判斷_owner對象是否為當前線程,如果是則令_recursions重入次數(shù)加一
如果當前實現(xiàn)是第一次獲取到鎖,則將_recursions設(shè)置為一
等待鎖釋放
阻塞和獲取鎖實現(xiàn)
將當前線程封裝為一個node節(jié)點,狀態(tài)設(shè)置為ObjectWaiter::TS_CXQ
將之添加到_cxp棧中,嘗試獲取鎖,如果獲取失敗,則將當前線程掛起,等待喚醒
喚醒以后,從掛起點執(zhí)行剩下的代碼
monitorexit釋放鎖實現(xiàn)
讓當前線程的_recursions重入次數(shù)減一,如果當前重入次數(shù)為0,則直接退出,喚醒其他線程
到此,相信大家對“Synchronized的原理介紹”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學習!
分享標題:Synchronized的原理介紹
轉(zhuǎn)載來源:http://www.chinadenli.net/article48/iiesep.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供微信小程序、定制開發(fā)、關(guān)鍵詞優(yōu)化、電子商務(wù)、面包屑導航、云服務(wù)器
聲明:本網(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)