欧美在线专区-欧美在线伊人-欧美在线一区二区三区欧美-欧美在线一区二区三区-pornodoxxx中国妞-pornodoldoo欧美另类

position>home>Softball

17張圖看穿synchronized關鍵字

[導讀]一文了解synchronized。張圖字

引子

小艾和小牛在路上相遇,看穿小艾一臉沮喪。關鍵

17張圖看穿synchronized關鍵字

小牛:小艾小艾,張圖字發(fā)生甚么事了?

小艾:別提了,看穿昨天有個面試官問了我好幾個關于 synchronized關鍵字的關鍵問題,沒答上來。張圖字

小艾:我后來查了很多資料,看穿有二十多頁的關鍵概念說明,也有三十來頁的張圖字源碼剖析,看得我頭大。看穿

小牛:你那看的關鍵是死知識,不好用,張圖字你得聽我的看穿總結。

小艾:看來是關鍵有備而來,那您給講講吧。

小牛:那咱們開始!


synchronized關鍵字引入

我們知道,在多線程程序中往往會出現(xiàn)這么一個情況:多個線程同時訪問某個線程間的共享變量。來舉個例子吧:

假設銀行存款業(yè)務寫了兩個方法,一個是存錢 store()方法 ,一個是查詢余額 get()方法。假設初始客戶小明的賬戶余額為 0 元。(PS:這個例子只是個 toy demo,為了方便大家理解寫的,真實的業(yè)務場景不會這樣。)

????//?account?客戶在銀行的存款?
????public?void?store(int?money){
????????int?newAccount=account+money;
????????account=newAccount;
????}
????public?void?get(){
????????System.out.print("小明的銀行賬戶余額:");
????????System.out.print(account);
????}

如果小明為自己存款 1 元,我們期望的線程調(diào)用情況如下:

  1. 首先會啟動一個線程調(diào)用 store()方法,為客戶賬戶余額增加 1;

  2. 再啟動一個線程調(diào)用 get()方法,輸出客戶的新余額為 1。

但實際情況可能由于線程執(zhí)行的先后順序,出現(xiàn)如圖所示的錯誤:

小明存錢流程
小明:咱家沒錢了

小明會驚奇的以為自己的錢沒存上。這就是一個典型的由共享數(shù)據(jù)引發(fā)的并發(fā)數(shù)據(jù)沖突問題

解決方式也很簡單,讓并發(fā)執(zhí)行會產(chǎn)生問題的代碼段不并發(fā)行了。

如果 store()方法 執(zhí)行完,才能執(zhí)行 get()方法,而不是像上圖一樣并發(fā)執(zhí)行,自然不會出現(xiàn)這個問題。那如何才能做到呢?

答案就是使用 synchronized關鍵字。

我們先從直覺上思考一下,如果要實現(xiàn)先執(zhí)行 store()方法,再執(zhí)行 get()方法的話該怎么設計。

我們可以設置某個鎖,鎖會有兩種狀態(tài),分別是上鎖解鎖。在 store()方法執(zhí)行之前,先觀察這個鎖的狀態(tài),如果是上鎖狀態(tài),就進入阻塞,代碼不運行;

如果這把鎖是解鎖狀態(tài),那就先將這把鎖狀態(tài)變?yōu)樯湘i,之后接著運行自己的代碼。運行完成之后再將鎖狀態(tài)設置為解鎖。

對于 get()方法也是如此。

Java 中的 synchronized關鍵字就是基于這種思想設計的。在 synchronized關鍵字中,鎖就是一個對象。

synchronized一共有三種使用方法:

  • 直接修飾某個實例方法。像上文代碼一樣,在這種情況下多線程并發(fā)訪問實例方法時,如果其他線程調(diào)用同一個對象的被 synchronized修飾的方法,就會被阻塞。相當于把鎖記錄在這個方法對應的對象上。

????//?account?客戶在銀行的存款?
????public?synchronized?void?store(int?money){
????????int?newAccount=account+money;
????????account=newAccount;
????}
????public?synchronized?void?get(){
????????System.out.print("小明的銀行賬戶余額:");
????????System.out.print(account);
????}
  • 直接修飾某個靜態(tài)方法。在這種情況下進行多線程并發(fā)訪問時,如果其他線程也是調(diào)用屬于同一類的被 synchronized修飾的靜態(tài)方法,就會被阻塞。相當于把鎖信息記錄在這個方法對應的類上。

????public?synchronized?static?void?get(){ 
????????···
????}
  • 修飾代碼塊。如果此時有別的線程也想訪問某個被synchronized(對象0)修飾的同步代碼塊時,也會被阻塞。

????public?static?void?get(){ 
????????synchronized(對象0){
????????????···
????????}
????}

小艾問:我看了不少參考書還有網(wǎng)上資料,都說 synchronized的鎖是鎖在對象上的。關于這句話,你能深入講講嗎?

小牛回答道:別急,我先講講 Java 對象在內(nèi)存中的表示。


Java 對象在內(nèi)存中的表示

講清 synchronized關鍵字的原理前需要理清 Java 對象在內(nèi)存中的表示方法。

Java 對象在內(nèi)存中的表示

上圖就是一個 Java 對象在內(nèi)存中的表示。我們可以看到,內(nèi)存中的對象一般由三部分組成,分別是對象頭、對象實際數(shù)據(jù)和對齊填充。

對象頭包含 Mark Word、Class Pointer和 Length 三部分。

  • Mark Word 記錄了對象關于鎖的信息,垃圾回收信息等。

  • Class Pointer 用于指向?qū)ο髮?Class 對象(其對應的元數(shù)據(jù)對象)的內(nèi)存地址。

  • Length只適用于對象是數(shù)組時,它保存了該數(shù)組的長度信息。

對象實際數(shù)據(jù)包括了對象的所有成員變量,其大小由各個成員變量的大小決定。

對齊填充表示最后一部分的填充字節(jié)位,這部分不包含有用信息。

我們剛才講的鎖 synchronized鎖使用的就是對象頭的 Mark Word 字段中的一部分。

Mark Word 中的某些字段發(fā)生變化,就可以代表鎖不同的狀態(tài)。

由于鎖的信息是記錄在對象里的,有的開發(fā)者也往往會說鎖住對象這種表述。

無鎖狀態(tài)的 Mark Word

這里我們以無鎖狀態(tài)的 Mark Word 字段舉例:

如果當前對象是無鎖狀態(tài),對象的 Mark Word 如圖所示。

無鎖狀態(tài)的 Mark Word 字段

我們可以看到,該對象頭的 Mark Word 字段分為四個部分:

  1. 對象的 hashCode ;

  2. 對象的分代年齡,這部分用于對對象的垃圾回收;

  3. 是否為偏向鎖位,1代表是,0代表不是;

  4. 鎖標志位,這里是 01。


synchronized關鍵字的實現(xiàn)原理

講完了 Java 對象在內(nèi)存中的表示,我們下一步來講講 synchronized關鍵字的實現(xiàn)原理。

從前文中我們可以看到, synchronized關鍵字有兩種修飾方法

  1. 直接作為關鍵字修飾在方法上,將整個方法作為同步代碼塊:

????public?synchronized?static?void?`get()`{ 
????????···
????}
  1. 修飾在同步代碼塊上

????public?static?void?`get()`{ 
????????synchronized(對象0){
????????????···
????????}
????}

針對這兩種情況,Java 編譯時的處理方法并不相同。

對于第一種情況,編譯器會為其自動生成了一個 ACC_SYNCHRONIZED關鍵字用來標識。

在 JVM 進行方法調(diào)用時,當發(fā)現(xiàn)調(diào)用的方法被 ACC_SYNCHRONIZED修飾,則會先嘗試獲得鎖。

對于第二種情況,編譯時在代碼塊開始前生成對應的1個 monitorenter指令,代表同步塊進入。2個 monitorexit指令,代表同步塊退出。

這兩種方法底層都需要一個 reference 類型的參數(shù),指明要鎖定和解鎖的對象。

如果 synchronized明確指定了對象參數(shù),那就是該對象。

如果沒有明確指定,那就根據(jù)修飾的方法是實例方法還是類方法,取對應的對象實例或類對象(Java 中類也是一種特殊的對象)作為鎖對象。

確定鎖定和解鎖的對象

每個對象維護著一個記錄著被鎖次數(shù)的計數(shù)器。當一個線程執(zhí)行 monitorenter,該計數(shù)器自增從 0 變?yōu)?1;

當一個線程執(zhí)行 monitorexit,計數(shù)器再自減。當計數(shù)器為 0 的時候,說明對象的鎖已經(jīng)釋放。

小艾問:為什么會有兩個 monitorexit指令呢?

小牛答:正常退出,得用一個 monitorexit吧,如果中間出現(xiàn)異常,鎖會一直無法釋放。所以編譯器會為同步代碼塊添加了一個隱式的 try-finally異常處理,在 finally中會調(diào)用 monitorexit命令最終釋放鎖。

重量級鎖

小艾問:那么問題來了,之前你說鎖的信息是記錄在對象的 Mark Word 中的,那現(xiàn)在冒出來的 monitor又是什么呢?

小牛答:我們先來看一下重量級鎖對應對象的 Mark Word。

在 Java 的早期版本中,synchronized鎖屬于重量級鎖,此時對象的 Mark Word 如圖所示。

重量級鎖的 Mark Word 字段

我們可以看到,該對象頭的 Mark Word 分為兩個部分。第一部分是指向重量級鎖的指針,第二部分是鎖標記位。

而這里所說的指向重量級鎖的指針就是 monitor

英文詞典翻譯 monitor是監(jiān)視器。Java 中每個對象會對應一個監(jiān)視器。

這個監(jiān)視器其實也就是監(jiān)控鎖有沒有釋放,釋放的話會通知下一個等待鎖的線程去獲取。

monitor的成員變量比較多,我們可以這樣理解:

monitor結構

我們可以將 monitor簡單理解成兩部分,第一部分表示當前占用鎖的線程,第二部分是等待這把鎖的線程隊列

如果當前占用鎖的線程把鎖釋放了,那就需要在線程隊列中喚醒下一個等待鎖的線程。

但是阻塞或喚醒一個線程需要依賴底層的操作系統(tǒng)來實現(xiàn),Java 的線程是映射到操作系統(tǒng)的原生線程之上的。

而操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)轉換到核心態(tài),這個狀態(tài)轉換需要花費很多的處理器時間,甚至可能比用戶代碼執(zhí)行的時間還要長。

由于這種效率太低,Java 后期做了改進,我再來詳細講一講。


CAS算法

在講其他改進之前,我們先來聊聊 CAS 算法。CAS 算法全稱為 Compare And Swap。

顧名思義,該算法涉及到了兩個操作,比較(Compare)和交換(Swap)。

怎么理解這個操作呢?我們來看下圖:

CAS 算法

我們知道,在對共享變量進行多線程操作的時候,難免會出現(xiàn)線程安全問題。

對該問題的一種解決策略就是對該變量加鎖,保證該變量在某個時間段只能被一個線程操作。

但是這種方式的系統(tǒng)開銷比較大。因此開發(fā)人員提出了一種新的算法,就是大名鼎鼎的 CAS 算法。

CAS 算法的思路如下:

  1. 該算法認為線程之間對變量的操作進行競爭的情況比較少。

  2. 算法的核心是對當前讀取變量值 E和內(nèi)存中的變量舊值 V進行比較。

  3. 如果相等,就代表其他線程沒有對該變量進行修改,就將變量值更新為新值 N

  4. 如果不等,就認為在讀取值 E到比較階段,有其他線程對變量進行過修改,不進行任何操作。

當線程運行 CAS 算法時,該運行過程是原子操作,原子操作的含義就是線程開始跑這個函數(shù)后,運行過程中不會被別的程序打斷。

我們來看看實際上 Java 語言中如何使用這個 CAS 算法,這里我們以 AtomicInteger類中的 compareAndSwapInt()方法舉例:

public?final?native?boolean?compareAndSwapInt
(Object?var1,?long?var2,?int?var3,?int?var4)

可以看到,該函數(shù)原型接受四個參數(shù):

  1. 第一個參數(shù)是一個 AtomicInteger對象。

  2. 第二個參數(shù)是該 AtomicInteger對象對應的成員變量在內(nèi)存中的地址。

  3. 第三個參數(shù)是上圖中說的線程之前讀取的值 P

  4. 第四個參數(shù)是上圖中說的線程計算的新值 V


偏向鎖

JDK 1.6 中提出了偏向鎖的概念。該鎖提出的原因是,開發(fā)者發(fā)現(xiàn)多數(shù)情況下鎖并不存在競爭,一把鎖往往是由同一個線程獲得的。

如果是這種情況,不斷的加鎖解鎖是沒有必要的。

那么能不能讓 JVM 直接負責在這種情況下加解鎖的事情,不讓操作系統(tǒng)插手呢?

因此開發(fā)者設計了偏向鎖。偏向鎖在獲取資源的時候,會在資源對象上記錄該對象是否偏向該線程。

偏向鎖并不會主動釋放,這樣每次偏向鎖進入的時候都會判斷該資源是否是偏向自己的,如果是偏向自己的則不需要進行額外的操作,直接可以進入同步操作。

下圖表示偏向鎖的 Mark Word結構:

偏向鎖的 Mark Word 字段

可以看到,偏向鎖對應的 Mark Word 包含該偏向鎖對應的線程 ID、偏向鎖的時間戳和對象分代年齡。

偏向鎖的申請流程

我們再來看一下偏向鎖的申請流程:

  1. 首先需要判斷對象的 Mark Word 是否屬于偏向模式,如果不屬于,那就進入輕量級鎖判斷邏輯。否則繼續(xù)下一步判斷;

  2. 判斷目前請求鎖的線程 ID 是否和偏向鎖本身記錄的線程 ID 一致。如果一致,繼續(xù)下一步的判斷,如果不一致,跳轉到步驟4;

  3. 判斷是否需要重偏向,重偏向邏輯在后面一節(jié)批量重偏向和批量撤銷會說明。如果不用的話,直接獲得偏向鎖;

  4. 利用 CAS 算法將對象的 Mark Word 進行更改,使線程 ID 部分換成本線程 ID。如果更換成功,則重偏向完成,獲得偏向鎖。如果失敗,則說明有多線程競爭,升級為輕量級鎖。

偏向鎖的申請流程

值得注意的是,在執(zhí)行完同步代碼后,線程不會主動去修改對象的 Mark Word,讓它重回無鎖狀態(tài)。

所以一般執(zhí)行完 synchronized語句后,如果是偏向鎖的狀態(tài)的話,線程對鎖的釋放操作可能是什么都不做。

匿名偏向鎖

在 JVM 開啟偏向鎖模式下,如果一個對象被新建,在四秒后,該對象的對象頭就會被置為偏向鎖。

一般來說,當一個線程獲取了一把偏向鎖時,會在對象頭和棧幀中的鎖記錄里不僅說明目前是偏向鎖狀態(tài),也會存儲鎖偏向的線程 ID。

在 JVM 四秒自動創(chuàng)建偏向鎖的情況下,線程 ID 為0。

由于這種情況下的偏向鎖不是由某個線程求得生成的,這種情況下的偏向鎖也稱為匿名偏向鎖。

批量重偏向和批量撤銷

生產(chǎn)者消費者模式下,生產(chǎn)者線程負責對象的創(chuàng)建,消費者線程負責對生產(chǎn)出來的對象進行使用。

當生產(chǎn)者線程創(chuàng)建了大量對象并執(zhí)行加偏向鎖的同步操作,消費者對對象使用之后,會產(chǎn)生大量偏向鎖執(zhí)行和偏向鎖撤銷的問題。

大量偏向鎖執(zhí)行和偏向鎖撤銷的問題

Russell K和 Detlefs D在他們的文章提出了批量重偏向和批量撤銷的過程。

在上圖情景下,他們探討了能不能直接將偏向的線程換成消費者的線程。

替換不是一件容易事,需要在 JVM 的眾多線程中找到類似上文情景的線程。

他們最后提出的解決方法是:

以類為單位,為每個類維護一個偏向鎖撤銷計數(shù)器,每一次該類的對象發(fā)生偏向撤銷操作時,該計數(shù)器計數(shù) +1,當這個計數(shù)值達到重偏向閾值時,JVM 就認為該類可能不適合正常邏輯,適合批量重偏向邏輯。這就是對應上圖流程圖里的是否需要重偏向過程。

以生產(chǎn)者消費者為例,生產(chǎn)者生產(chǎn)同一類型的對象給消費者,然后消費者對這些對象都需要執(zhí)行偏向鎖撤銷,當撤銷過程過多時就會觸發(fā)上文規(guī)則,JVM 就注意到這個類了。

批量重偏向和批量撤銷

具體規(guī)則是:

  1. 每個類對象會有一個對應的 epoch字段,每個處于偏向鎖狀態(tài)對象的 Mark Word 中也有該字段,其初始值為創(chuàng)建該對象時,類對象中的 epoch的值。

  2. 每次發(fā)生批量重偏向時,就將類對象的 epoch字段 +1,得到新的值 epoch_new

  3. 遍歷 JVM 中所有線程的棧,找到該類對象,將其 epoch字段改為新值。根據(jù)線程棧的信息判斷出該線程是否鎖定了該對象,將現(xiàn)在偏向鎖還在被使用的對象賦新值 epoch_new

  4. 下次有線程想獲得鎖時,如果發(fā)現(xiàn)當前對象的 epoch值和類的 epoch不相等,不會執(zhí)行撤銷操作,而是直接通過 CAS 操作將其 Mark Word 的 Thread ID 改成當前線程 ID。

批量撤銷相對于批量重偏向好理解得多,JVM 也會統(tǒng)計重偏向的次數(shù)。

假設該類計數(shù)器計數(shù)繼續(xù)增加,當其達到批量撤銷的閾值后(默認40),JVM 就認為該類的使用場景存在多線程競爭,會標記該類為不可偏向,之后對于該類的鎖升級為輕量級鎖。


輕量級鎖

輕量級鎖的設計初衷在于并發(fā)程序開發(fā)者的經(jīng)驗“對于絕大部分的鎖,在整個同步周期內(nèi)都是不存在競爭的”。

所以它的設計出發(fā)點也在線程競爭情況較少的情況下。我們先來看一下輕量級鎖的 Mark Word 布局。

如果當前對象是輕量級鎖狀態(tài),對象的 Mark Word 如下圖所示。

輕量級鎖 Mark Word 字段

我們可以看到,該對象頭Mark Word分為兩個部分。第一部分是指向棧中的鎖記錄的指針,第二部分是鎖標記位,針對輕量級鎖該標記位為 00。

小艾問:那這指向棧中的鎖記錄的指針是什么意思呢?

小牛答:這得結合輕量級鎖的上鎖步驟來慢慢講。

如果當前這個對象的鎖標志位為 01(即無鎖狀態(tài)或者輕量級鎖狀態(tài)),線程在執(zhí)行同步塊之前,JVM 會先在當前的線程的棧幀中創(chuàng)建一個 Lock Record,包括一個用于存儲對象頭中的 Mark Word 以及一個指向?qū)ο蟮闹羔槨?/p>

Lock Record

然后 JVM 會利用 CAS 算法對這個對象的 Mark Word 進行修改。如果修改成功,那該線程就擁有了這個對象的鎖。我們來看一下如果上圖的線程執(zhí)行 CAS 算法成功的結果。

執(zhí)行 CAS 算法

當然 CAS 也會有失敗的情況。如果 CAS 失敗,那就說明同時執(zhí)行 CAS 操作的線程可不止一個了, Mark Word 也做了更改。

首先虛擬機會檢查對象的 Mark Word 字段指向棧中的鎖記錄的指針是否指向當前線程的棧幀。如果是,那就說明可能出現(xiàn)了類似 synchronized中套 synchronized情況:

synchronized?(對象0)?{ 
????synchronized?(對象0)?{
????????···
????}
}

當然這種情況下當前線程已經(jīng)擁有這個對象的鎖,可以直接進入同步代碼塊執(zhí)行。

否則說明鎖被其他線程搶占了,該鎖還需要升級為重量級鎖。

和偏向鎖不同的是,執(zhí)行完同步代碼塊后,需要執(zhí)行輕量級鎖的解鎖過程。解鎖過程如下:

  1. 通過 CAS 操作嘗試把線程棧幀中復制的 Mark Word 對象替換當前對象的 Mark Word。

  2. 如果 CAS 算法成功,整個同步過程就完成了。

  3. 如果 CAS 算法失敗,則說明存在競爭,鎖升級為重量級鎖。

我們來總結一下輕量級鎖升級過程吧:

輕量級鎖的升級過程

總結

這次我們了解了 synchronized底層實現(xiàn)原理和對應的鎖升級過程。最后我們再通過這張流程圖來回顧一下 synchronized鎖升級過程吧。

鎖申請完整流程

巨人肩膀
  1. 實現(xiàn)Java虛擬機:JVM故障診斷與性能優(yōu)化

  2. 深入理解java虛擬機 JVM高級特性與最佳實踐

  3. Russell K , Detlefs D . Eliminating synchronization-related atomic operations with biased locking and bulk rebiasing[C]// Acm Sigplan Conference on Object-oriented Programming Systems. ACM, 2006.

  4. Dice D , Moir M S , Scherer Iii W N . Quickly reacquirable locks: US 2010.

  5. https://github.com/farmerjohngit/myblog/issues/12

  6. https://www.itqiankun.com/article/bias-lightweight-synchronized-lock

  7. https://www.itqiankun.com/article/bias-lock-epoch-effect

  8. https://www.hollischuang.com/archives/1883

  9. http://www.ideabuffer.cn/2017/05/06/Java%E5%AF%B9%E8%B1%A1%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80/

  10. http://www.ideabuffer.cn/2017/04/21/java-%E4%B8%AD%E7%9A%84%E9%94%81-%E5%81%8F%E5%90%91%E9%94%81%E3%80%81%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E3%80%81%E8%87%AA%E6%97%8B%E9%94%81%E3%80%81%E9%87%8D%E9%87%8F%E7%BA%A7%E9%94%81/

  11. https://blog.csdn.net/zhao_miao/article/details/84500771


推薦閱讀
帶寬、延時、吞吐率、PPS 這些都是啥?
讀者問:小林怎么學操作系統(tǒng)和計算機網(wǎng)絡呀?
讀者問:小林你的 500 張圖是怎么畫的?
讀者問:小林你能分享做公眾號的經(jīng)驗嗎?

免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

Popular articles

主站蜘蛛池模板: 亚洲免费观看视频| 真希友田视频中文字幕在线看| 欧美日韩三级在线观看| 动漫洗濯屋| 久久国产精品-国产精品| 久久依人| 最近的中文字幕视频完整| 亚洲一区电影在线观看| 国内精品久久久久久久影视麻豆| 久久九九99热这里只有精品 | 里番牝教师~淫辱yy608| 又爽又黄又无遮挡的视频| 最好看的免费观看视频 | 三级毛片在线看| 老汉扛起娇妻玉腿进入h文| 国产凌凌漆国语| 国产性生大片免费观看性 | 欧美日韩久久中文字幕| 久久精品欧美日韩精品| 中文japanese在线播放| 亚洲国产天堂久久综合2261144 | 国产精品美女久久久久| 久久久久久福利| 奇米视频7777| 上原亚衣一区二区在线观看| 外出电影| 欧美特黄一片aa大片免费看| 中文字幕在线播放| 能播放18xxx18女同| 日韩a毛片免费观看| 动漫美女和男人羞羞漫画| 国产在线播放免费| 全彩无翼口工漫画大全3d| 国产极品视觉盛宴| 一区二区三区午夜| 高嫁肉柳风车动漫| 欧美在线不卡| 国产三级精品三级在线观看| 欧美乱大交xxxxx| 182tv精品视频在线播放| 美女把腿扒开让男人桶爽了 |