您好,登錄后才能下訂單哦!
本篇內容主要講解“Synchronized的原理介紹”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Synchronized的原理介紹”吧!
CAS
全稱:CompareAndSwap
,故名思意:比較并交換。他的主要思想就是:**我需要對一個值進行修改,我不會直接修改,而是將當前我認為的值和要修改的值傳入,如果此時內存中的確為我認為的值,那么就進行修改,否則修改失敗。**他的思想是一種樂觀鎖的思想。
一張圖解釋他的工作流程:
知道了它的工作原理,我們來聽一個場景:現在有一個int
類型的數字它等于1,存在三個線程需要對其進行自增操作。
一般來說,我們認為的操作步驟是這樣:線程從主內存中讀取這個變量,到自己的工作空間中,然后執行變量自增,然后回寫主內存,但這樣在多線程狀態下會存在安全問題。而如果我們保證變量的安全性,常用的做法是ThreadLocal
或者直接加鎖。(對ThreadLocal
不了解的兄弟,看我這篇文章一文讀懂ThreadLocal設計思想)
這個時候我們思考一下,如果使用我們上面的CAS
進行對值的修改,我們需要如何操作。
首先,我們需要將當前線程認為的值傳入,然后將想要修改的值傳入。如果此時內存中的值和我們的期望值相等,進行修改,否則修改失敗。這樣是不是解決了一個多線程修改的問題,而且它沒有使用到操作系統提供的鎖。
上面的流程其實就是類AtomicInteger
執行自增操作的底層實現,它保證了一個操作的原子性。我們來看一下源碼。
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //從內存中讀取最新值 var5 = this.getIntVolatile(var1, var2); //修改 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
實現CAS
使用到了Unsafe
類,看它的名字就知道不安全,所以JDK
不建議我們使用。對比我們上面多個線程執行一個變量的修改流程,這個類的操作僅僅增加了一個自旋,它在不斷獲取內存中的最新值,然后執行自增操作。
可能有兄弟說了,那getIntVolatile
和compareAndSwapInt
操作如何保證原子性。
對于getIntVolatile
來說,讀取內存中的地址,本來就一部操作,原子性顯而易見。
對于compareAndSwapInt
來說,它的原子性由CPU
保證,通過一系列的CPU
指令實現,其C++
底層是依賴于Atomic::cmpxchg_ptr
實現的
到這里CAS
講完了,不過其中還有一個ABA
問題,有興趣可以去了解我的這篇文章多線程知識點小節。里面有詳細的講解。
我們通過CAS
可以保證了操作的原子性,那么我們需要考慮一個東西,鎖是怎么實現的。對比生活中的case
,我們通過一組密碼或者一把鑰匙實現了一把鎖,同樣在計算機中也通過一個鑰匙即synchronized
代碼塊使用的鎖對象。
那其他線程如何判斷當前資源已經被占有了呢?
在計算機中的實現,往往是通過對一個變量的判斷來實現,無鎖狀態為0
,有鎖狀態為1
等等來判斷這個資源是否被加鎖了,當一個線程釋放鎖時僅僅需要將這個變量值更改為0,代表無鎖。
我們僅僅需要保證在進行變量修改時的原子性即可,而剛剛的CAS
剛好可以解決這個問題
至于那個鎖變量存儲在哪里這個問題,就是下面的內容了,對象的內存布局
各位兄弟們,應該都清楚,我們創建的對象都是被存放到堆中的,最后我們獲得到的是一個對象的引用指針。那么有一個問題就會誕生了,JVM
創建的對象的時候,開辟了一塊空間,那這個空間里都有什么東西?這個就是我們這個點的內容。
先來結論:Java
中存在兩種類型的對象,一種是普通對象,另一種是數組
對象內存布局
我們來一個一個解釋其含義。
**白話版:**對象頭中包含又兩個字段,Mark Word
主要存儲改對象的鎖信息,GC
信息等等(鎖升級的實現)。而其中的Klass Point
代表的是一個類指針,它指向了方法區中類的定義和結構信息。而Instance Data
代表的就是類的成員變量。在我們剛剛學習Java
基礎的時候,都聽過老師講過,對象的非靜態成員屬性都會被存放在堆中,這個就是對象的Instance Data
。相對于對象而言,數組額外添加了一個數組長度的屬性
最后一個對其數據是什么?
我們拿一個場景來展示這個原因:**想像一下,你和女朋友周末打算出去玩,女朋友讓你給她帶上口紅,那么這個時候你僅僅會帶上口紅嘛?當然不是,而是將所有的必用品統統帶上,以防剛一出門就得回家拿東西!!!**這種行為叫啥?未雨綢繆,沒錯,暖男行為。還不懂?再來一個案例。你準備創業了,資金非常充足,你需要注冊一個域名,你僅僅注冊一個嘛?不,而是將所有相關的都注冊了,防止以后大價錢買域名。一個道理。
而對于CPU
而言,它在進行計算處理數據的時候,不可能需要什么拿什么吧,那對其性能損耗非常嚴重。所以有一個協議,CPU
在讀取數據的時候,不僅僅只拿需要的數據,而是獲取一行的數據,這就是緩存行,而一行是64個字節。
所以呢?通過這個特性可以玩一些詭異的花樣,比如下面的代碼。
public class CacheLine { private volatile Long l1 , l2; }
我們給一個場景:兩個線程t1和t2
分別操作l1
和l2
,那么當t1
對l1
做了修改以后,l2
需不需要重新讀取主內存種值。答案是一定,根據我們上面對于緩存行的理解,l1和l2
必然位于同一個緩存行中,根據緩存一致性協議,當數據被修改以后,其他CPU
需要重新重主內存中讀取數據。這就引發了偽共享的問題
那么為什么對象頭要求會存在一個對其數據呢?
HotSpot
虛擬機要求每一個對象的內存大小必須保證為8字節的整數倍,所以對于不是8字節的進行了對其補充。其原因也是因為緩存行的原因
對象=對象頭+實例數據
我們在前面聊了一下,計算機中的鎖的實現思路和對象在內存中的布局,接下來我們來聊一下它的具體鎖實現,為對象加鎖使用的是對象內存模型中的對象頭,通過對其鎖標志位和偏向鎖標志位的修改實現對資源的獨占即加鎖操作。接下來我們看一下它的內存結構圖。
上圖就是對象頭在內存中的表現(64位),JVM
通過對對象頭中的鎖標志位和偏向鎖位的修改實現“無鎖”。
對于無鎖這個概念來說,在1.6
之前,即所有的對象,被創建了以后都處于無鎖狀態,而在1.6
之后,偏向鎖被開啟,對象在經歷過幾秒的時候(4~5s)以后,自動升級為當前線程的偏向鎖。(無論經沒經過synchronized
)。
我們來驗證一下,通過jol-core
工具打印其內存布局。注:該工具打印出來的數據信息是反的,即最后幾位在前面,通過下面的案例可以看到
場景:創建兩個對象,一個在剛開始的時候就創建,另一個在5
秒之后創建,進行對比其內存布局
Object object = new Object(); System.out.println(ClassLayout.parseInstance(object).toPrintable());//此時處于無鎖態 try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} Object o = new Object(); System.out.println("偏向鎖開啟"); System.out.println(ClassLayout.parseInstance(o).toPrintable());//五秒以后偏向鎖開啟
我們可以看到,線程已開啟創建的對象處于無鎖態,而在5秒以后創建的線程處于偏向鎖狀態。
同樣,當我們遇到synchronized
塊的時候,也會自動升級為偏向鎖,而不是和操作系統申請鎖。
說完這個,提一嘴一個面試題吧。解釋一下什么是無鎖。
從對象內存結構的角度來說,是一個鎖標志位的體現;從其語義來說,無鎖這個比較抽象了,因為在以前鎖的概念往往是與操作系統的鎖息息相關,所以新出現的基于CAS
的偏向鎖,輕量級鎖等等也被成為無鎖。而在synchronized
升級的起點----無鎖。這個東西就比較難以解釋,只能說它沒加鎖。不過面試的過程中從對象內存模型中理解可能會更加舒服一點。
在實際開發中,往往資源的競爭比較少,于是出現了偏向鎖,故名思意,當前資源偏向于該線程,認為將來的一切操作均來自于改線程。下面我們從對象的內存布局下看看偏向鎖
對象頭描述:偏向鎖標志位通過CAS修改為1,并且存儲該線程的線程指針
當發生了鎖競爭,其實也不算鎖競爭,就是當這個資源被多個線程使用的時候,偏向鎖就會升級。
在升級的期間有一個點-----全局安全點,只有處在這個點的時候,才會撤銷偏向鎖。
全局安全點-----類似于CMS
的stop the world
,保證這個時候沒有任何線程在操作這個資源,這個時間點就叫做全局安全點。
可以通過XX:BiasedLockingStartupDelay=0
關閉偏向鎖的延遲,使其立即生效。
通過XX:-UseBiasedLocking=false
關閉偏向鎖。
在聊輕量級鎖的時候,我們需要搞明白這幾個問題。什么是輕量級鎖,什么重量級鎖?,為什么就重量了,為什么就輕量了?
輕量級和重量級的標準是依靠于操作系統作為標準判斷的,在進行操作的時候你有沒有調用過操作系統的鎖資源,如果有就是重量級,如果沒有就是輕量級
接下來我們看一下輕量級鎖的實現。
線程獲取鎖,判斷當前線程是否處于無鎖或者偏向鎖的狀態,如果是,通過CAS
復制當前對象的對象頭到Lock Recoder
放置到當前棧幀中(對于JVM
內存模型不清楚的兄弟,看這里入門JVM看這一篇就夠了
通過CAS
將當前對象的對象頭設置為棧幀中的Lock Recoder
,并且將鎖標志位設置為00
如果修改失敗,則判斷當前棧幀中的線程是否為自己,如果是自己直接獲取鎖,如果不是升級為重量級鎖,后面的線程阻塞
我們在上面提到了一個Lock Recoder
,這個東東是用來保存當前對象的對象頭中的數據的,并且此時在該對象的對象頭中保存的數據成為了當前Lock Recoder
的指針
我們看一個代碼模擬案例,
public class QingLock { public static void main(String[] args) { try { //睡覺5秒,開啟偏向鎖,可以使用JVM參數 TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} A o = new A(); //讓線程交替執行 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()); } } } }
運行結果為兩個線程交替前后
輕量級鎖強調的是線程交替使用資源,無論線程的個數有幾個,只要沒有同時使用就不會升級為重量級鎖
在上面的關于輕量級鎖加鎖步驟的講解中,如果線程CAS
修改失敗,則判斷棧幀中的owner
是不是自己,如果不是就失敗升級為重量級鎖,而在實際中,JDK
加入了一種機制自旋鎖,即修改失敗以后不會立即升級而是進行自旋,在JDK1.6
之前自旋次數為10
次,而在1.6
又做了優化,改為了自適應自旋鎖,由虛擬機判斷是否需要進行自旋,判斷原因有:當前線程之前是否獲取到過鎖,如果沒有,則認為獲取鎖的幾率不大,直接升級,如果有則進行自旋獲取鎖。
前面我們談到了無鎖-->偏向鎖-->輕量級鎖,現在最后我們來聊一下重量級鎖。
這個鎖在我們開發過程中很常見,線程搶占資源大部分都是同時的,所以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(); } } }
未進入代碼塊之前,兩者均為無鎖狀態
開始執行循環,進入代碼塊
在看一眼,對象頭鎖標志位
對比上圖,可以發現,在線程競爭的時候鎖,已經變為了重量級鎖。接下來我們來看一下重量級鎖的實現
我們先從Java
字節碼分析synchronzied
的底層實現,它的主要實現邏輯是依賴于一個monitor
對象,當前線程執行遇到monitorenter
以后,給當前對象的一個屬性recursions
加一(下面會詳細講解),當遇到monitorexit
以后該屬性減一,代表釋放鎖。
代碼
Object o = new Object(); synchronized (o){ }
匯編碼
上圖就是上面的四行代碼的匯編碼,我們可以看到synchronized
的底層是兩個匯編指令
monitoreneter
代表synchronized
塊開始
monitorexit
代表synchronized
塊結束
有兄弟要說了為什么會有兩個monitorexit
?這也是我曾經遇到的一個面試題
第一個monitorexit
代表了synchronized
塊正常退出
第二個monitorexit
代表了synchronized
塊異常退出
很好理解,當在synchronized
塊中出現了異常以后,不能當前線程一直拿著鎖不讓其他線程使用吧。所以出現了兩個monitorexit
同步代碼塊理解了,我們再來看一下同步方法。
代碼
public static void main(String[] args) { } public synchronized void test01(){ }
匯編碼
我們可以看到,同步方法增加了一個ACC_SYNCHRONIZED
標志,它會在同步方法執行之前調用monitorenter
,結束以后調用monitorexit
指令。
在Java
匯編碼的講解中,我們提到了兩個指令monitorenter
和monitorexit
,其實他們是來源于一個C++
對象monitor
,在Java
中每創建一個對象的時候都會有一個monitor
對象被隱式創建,他們和當前對象綁定,用于監視當前對象的狀態。其實說綁定也不算正確,其實際流程為:線程本身維護了兩個MonitorList
列表,分別為空閑(free)和已經使用(used),當線程遇到同步代碼塊或者同步方法的時候,會從空閑列表中申請一個monitor
使用,如果當先線程已經沒有空閑的了,則直接從全局(JVM
)獲取一個monitor
使用
我們來看一下C++
對這個對象的描述
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 重入次數 _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
棧中,嘗試獲取鎖,如果當前線程獲得鎖就執行代碼,如果沒有獲取到鎖則添加到EntryList
阻塞隊列中
如果在執行的過程的當前線程被掛起(wait
)則被添加到WaitSet
等待隊列中,等待被喚醒繼續執行
當同步代碼塊執行完畢以后,從_cxp
或者EntryList
中獲取一個線程執行
monitorenter
加鎖實現
CAS
修改當前monitor
對象的_owner
為當前線程,如果修改成功,執行操作;
如果修改失敗,判斷_owner
對象是否為當前線程,如果是則令_recursions
重入次數加一
如果當前實現是第一次獲取到鎖,則將_recursions
設置為一
等待鎖釋放
阻塞和獲取鎖實現
將當前線程封裝為一個node
節點,狀態設置為ObjectWaiter::TS_CXQ
將之添加到_cxp
棧中,嘗試獲取鎖,如果獲取失敗,則將當前線程掛起,等待喚醒
喚醒以后,從掛起點執行剩下的代碼
monitorexit
釋放鎖實現
讓當前線程的_recursions
重入次數減一,如果當前重入次數為0,則直接退出,喚醒其他線程
到此,相信大家對“Synchronized的原理介紹”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。