自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面試中如何答好:synchronized

開發(fā) 前端
當(dāng)線程退出Monitor鎖,便會將Monitor鎖中的_count減1,清空_owner,jvm會隨機(jī)喚醒_EntryList集合中一個線程重新獲取Monitor鎖,這個隨機(jī)便突出了synchronized的不公平性。

前置思考

實(shí)現(xiàn)鎖應(yīng)該考慮的問題

  1. 如何獲取資源(鎖)?
  2. 獲取不到資源的線程如何處理?
  3. 如何釋放資源?
  4. 資源釋放后如何讓其他線程獲取資源?

由此可以得出實(shí)現(xiàn)一把鎖,應(yīng)該具備哪些邏輯

  • 鎖的標(biāo)識
    需要有個標(biāo)識或者狀態(tài)來表示鎖是否已經(jīng)被占用。
  • 線程搶鎖的邏輯
    多個線程如何搶鎖,如何才算搶到鎖,已經(jīng)搶到鎖的線程再次搶鎖如何處理等等。
  • 線程掛起的邏輯
    線程如果搶到鎖自然順利往下運(yùn)行了,而那些沒有搶到鎖的線程怎么處理呢?如果一直處于活躍狀態(tài),cpu肯定是吃不消,那就需要掛起。具體又如何掛起呢?
  • 線程存儲機(jī)制
    沒有搶到鎖的線程就掛起了,而且被掛起的線程可能有很多個,這些線程總要放在某個地方保存起來等待喚醒,然而這么多被掛起的線程,要喚醒哪一個呢?這就需要一套保存機(jī)制來支撐喚醒邏輯。
  • 線程釋放鎖的邏輯
    線程在執(zhí)行完后就要釋放鎖,跟搶鎖邏輯是對應(yīng)的,其實(shí)也是操作鎖標(biāo)識。
  • 線程喚醒的邏輯
    鎖釋放后,就要去喚醒被阻塞的線程,這就要考慮喚醒誰,如何喚醒,喚醒后的線程做什么事情。

這是我們在探討AQS時候的思考,這里也把它貼過來,是因?yàn)橥瑯邮擎i,基本的實(shí)現(xiàn)思路都是一樣的,也方便對比兩個鎖的操作,同樣帶著上面的思考,我們來看看synchronized是怎么處理的

從synchronized的使用入手

  1. synchronized可以修飾實(shí)例方法。
  2. synchronized可以修飾靜態(tài)方法。
  3. synchronized可以修飾實(shí)例方法的代碼塊。
  4. synchronized可以修飾靜態(tài)方法的代碼塊。

怎么使用不必多說,本篇就以修飾實(shí)例方法中的代碼塊為例來看下它的底層邏輯,如下為代碼塊代碼。

synchronized(this){
            
}

synchronized是jvm級別實(shí)現(xiàn)的,在java端只是一個關(guān)鍵字,它在編譯后會變?yōu)槿缦聢D的樣子,整個代碼塊前后被monitorenter 和 monitorexit包裹,也就是說synchronized關(guān)鍵字的在編譯后就變?yōu)樯厦鎯蓚€關(guān)鍵字。

圖片圖片

jvm運(yùn)行代碼的時候,當(dāng)遇到這兩個關(guān)鍵字時候就會做出相應(yīng)的處理。

其實(shí)很簡單,這兩個關(guān)鍵字無非是在邏輯前后分別進(jìn)行搶鎖和釋放鎖,與ReentrantLock的lock和unlock是一個道理。

monitorenter 和 monitorexit這兩個是jvm級別字節(jié)碼指令,不難想到j(luò)vm在運(yùn)行代碼的時候,遇到monitorenter關(guān)鍵字,一定會啟動搶鎖的邏輯,包括搶鎖,入隊,阻塞;而遇到monitorexit的時候一定會走釋放鎖邏輯,包括釋放鎖,喚醒阻塞線程。

前置了解

創(chuàng)建對象

通過synchronized代碼塊的用法我們就能知道java的synchronized使用是依賴一個對象的,所以我們下面先看下java中的對象是怎么創(chuàng)建的。

java中如何創(chuàng)建一個對象?java代碼會被編譯成字節(jié)碼然后被jvm運(yùn)行,jvm在遇到new關(guān)鍵字的時候就會啟動對象的創(chuàng)建流程,對象的大致流程如下:

圖片圖片

默認(rèn)情況下jvm加載類是懶加載的,所以創(chuàng)建對象的第一步是判斷類是否已經(jīng)加載,如果沒有加載,需要先走類的加載流程。

接下來是分配內(nèi)存,一個對象在類加載的時候就可以知道所需要的內(nèi)存大小,此時就是在堆中劃分一塊區(qū)域出來作為對象的私密空間,具體如何分配和具體使用的垃圾回收器有關(guān),jvm篇再細(xì)講,在偌大的堆中怎么為一個對象劃分區(qū)間呢?這里的分配主要是兩種方法:指針碰撞和空閑列表,但是不管哪種劃分方法都會存在并發(fā)問題,此時jvm的解決方案是TLAB和cas配合失敗重試。

初始化零值這一步是給對象中的屬性賦零值,比如int類型默認(rèn)為0,這一步是避免屬性不賦值的情況下出現(xiàn)空指針異常。

每個對象都會有一個對象頭區(qū)域,這個區(qū)域包括Mark Word,元數(shù)據(jù)指針,數(shù)組長度三個部分,Mark Word用于保存對象的運(yùn)行時數(shù)據(jù),比如hashcode,分代年齡,鎖標(biāo)識等,元數(shù)據(jù)指針是當(dāng)前對象所屬類對象的地址,只有數(shù)組對象才會有數(shù)組長度。

最后初始化對象,這個時候一個完整的對象生成了。

一個完整對象的結(jié)構(gòu)如下:

圖片圖片

可以看到結(jié)構(gòu)中有一個對齊填充,對齊填充是為了滿足對象的大小為8字節(jié)的整數(shù)倍,只有8字節(jié)的整數(shù)倍才是最高效的存取方式。所以一個對象的大小總是8字節(jié)的整數(shù)倍。

對象頭

對象頭是對象中用于保存實(shí)例數(shù)據(jù)外的運(yùn)行時數(shù)據(jù)的區(qū)域。

我們知道java是面向?qū)ο蟮?,在java的世界一切皆對象,所以整個jvm的設(shè)計都是圍繞對象,包括對象所屬類的加載,對象的創(chuàng)建,對象的保存,對象的銷毀,對象的回收,鎖的實(shí)現(xiàn),以及jvm的內(nèi)存結(jié)構(gòu)等等都要圍繞對象設(shè)計,這就導(dǎo)致對象自身會有很多的運(yùn)行時數(shù)據(jù),比如垃圾回收依賴的分代年齡,代碼運(yùn)行過程中用于標(biāo)識對象唯一的hashcode,當(dāng)用作鎖對象的時候鎖的相關(guān)信息存儲,記錄當(dāng)前對象所屬的類對象指針等等。

所以jvm設(shè)計了對象頭,對象頭包括Mark Word,MetaDate,數(shù)組長度三部分。

Monitor

jvm對于鎖的設(shè)計是監(jiān)視器鎖,當(dāng)對象作為鎖的時候,jvm會為該對象關(guān)聯(lián)一個Monitor對象(一一對應(yīng)),這個Monitor對象就是該對象攜帶的一個監(jiān)視器。

這個Monitor對象是jvm級別實(shí)現(xiàn)的,是一個jvm級別的對象,所以我們在java端開發(fā)的時候是看不到摸不到的,但卻是真實(shí)存在的。

Monitor對象結(jié)構(gòu)如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄個數(shù)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 占用資源的線程
    _WaitSet      = NULL; // 處于wait狀態(tài)的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 處于等待鎖block狀態(tài)的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

synchronized鎖在優(yōu)化前就是一個地地道道的重量級鎖,重量級鎖的實(shí)現(xiàn)其實(shí)就是依賴于多線程爭奪Monitor鎖的擁有權(quán),而Monitor鎖的實(shí)現(xiàn)則依賴于操作系統(tǒng)底層的互斥原語mutex,因此每一次獲取Monitor鎖的時候都會經(jīng)過用戶態(tài)到內(nèi)核態(tài)的切換,性能很低。這便是重量鎖的由來。

實(shí)現(xiàn)原理

上面做了一些相關(guān)知識的介紹,可能還比較碎片化,接下來我們就通過加鎖流程將所有信息都串聯(lián)起來。

1.5的鎖

在jdk1.6之前的鎖就是單純的Monitor鎖,所以性能是很差的,1.6開始對鎖做了優(yōu)化,性能才得到提升。

1.6的鎖

JVM內(nèi)置鎖在1.5之后版本做了如下重大的優(yōu)化,在做了優(yōu)化后,其性能顯著提高,基本與ReentrantLock保持同等性能。

  • 鎖粗化
  • 鎖消除
  • 鎖升級:輕量級鎖 偏向鎖 適應(yīng)性自旋

接下來由淺入深講解優(yōu)化內(nèi)容

1. 鎖消除

鎖的消除,顧名思義就是將鎖去除,因?yàn)橛行﹫鼍跋骆i是可以去除的

public void sync()  {
        Sync sync=new Sync();
        synchronized(sync){

        }
}

如上這種情況,我們知道進(jìn)出一個方法就是當(dāng)前線程棧的入棧出棧,所以方法內(nèi)部只要不涉及共享資源操作就是線程安全的,如上這段代碼,sync對象聲明在方法內(nèi)部,其引用是局部變量,是線程獨(dú)享資源不是共享資源,是線程獨(dú)有資源,隨著出棧發(fā)生,對象也就銷毀了,因此此處是可以不用加鎖的,鎖消除優(yōu)化就是對這種情況進(jìn)行去除鎖的處理。

jvm如何進(jìn)行優(yōu)化的呢?jvm在JIT編譯時(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時進(jìn)行編譯,又稱即時編譯),通過對運(yùn)行上下文的掃描,通過逃逸分析判斷方法中的是否存在共享資源,如果無共享資源則去除不存在共享資源競爭的鎖,從而節(jié)省請求鎖時間。

典型的案例是StringBuffer的使用,后續(xù)會講解。

通過上面我們知道鎖消除依賴逃逸分析,逃逸分析是可以通過jvm參數(shù)配置的,如下:

-XX:+DoEscapeAnalysis 開啟逃逸分析

-XX:+EliminateLocks 開啟鎖消除

逃逸分析可以簡單理解為分析資源是否能逃逸到其他方法或者其他線程中。

2. 鎖粗化

顧名思義,把小范圍的多個鎖變成大范圍少個鎖。

public Sync sync=new Sync();

public void sync()  {
        
        synchronized(sync){

        }
        synchronized(sync){

        }
        synchronized(sync){

        }
}

上面的代碼可以看出,一個邏輯被多次加同一把鎖,每一次上鎖都是會耗時的,所以完全可以把多個鎖合并為一個鎖,這樣只需要上一次鎖就可以了,大大節(jié)省了時間。

同樣jvm在即時編譯的時候會掃描判斷是否存在可以粗化的鎖行為。

3. 鎖膨脹

鎖膨脹又叫鎖升級。

鎖升級是鎖優(yōu)化后的鎖機(jī)制,這個機(jī)制中包含這樣幾個概念:偏向鎖,輕量級鎖,適應(yīng)性自旋,重量級鎖。

鎖升級是依靠對象頭的Mark Word來保存標(biāo)志信息的,接下來以32位操作系統(tǒng)來看下鎖升級過程中的對象頭中運(yùn)行時數(shù)據(jù)的變化。

圖片圖片

  • 無鎖狀態(tài) 當(dāng)沒有任何線程進(jìn)入的時候,此時處于無鎖狀態(tài),Mark Word中會有25bit的空間大小留給hashcode,4bit的空間大小留給對象的分代年齡信息,1bit的空間大小是標(biāo)識是否偏向(0否,1是),2bit的空間大小是鎖標(biāo)識位。

此時鎖標(biāo)志位為01,是否偏向?yàn)?,代表無鎖狀態(tài)。但是此時并不一定有hashcode,因?yàn)閔ashcode是代碼運(yùn)行過程中調(diào)用生成方法才生成的,如果運(yùn)行過程不調(diào)用就不會生成。

請注意,hashcode的生成是會影響鎖的升級過程的。

  • 偏向鎖狀態(tài)

當(dāng)?shù)谝粋€線程T1進(jìn)入代碼塊后的步驟(前提條件是全程無hashcode生成)

  1. 判斷是否處于偏向中(通過Mark Word中的是否偏向判斷)
  2. 此時未處于偏向中,當(dāng)前線程會將自己的線程id保存到Mark Word中,設(shè)置是否偏向?yàn)?,此時鎖標(biāo)志位依然是01

此時Mark Word為:23bit的線程id,4bit的分代年齡,是否偏向?yàn)?,鎖標(biāo)識位依然為01。

此時是偏向鎖狀態(tài),它其實(shí)是一種特殊的無鎖狀態(tài)。

上面的過程是建立在全程無hashcode生成的基礎(chǔ)上,我們知道了hashcode會占用25bit,線程id會占用23bit,如果過程有hashcode生成怎么辦,這里涉及到兩個問題。

第一個問題,T1進(jìn)入前就已經(jīng)生成了hashcode怎么處理?

jvm的做法是如果偏向前已經(jīng)生成hashcode,那么就放棄偏向,直接進(jìn)入輕量級鎖。

第二個問題,T1進(jìn)入后鎖狀態(tài)變?yōu)榱似蜴i,此時生成hashcode怎么處理?

jvm的做法是撤銷偏向,直接進(jìn)入重量級鎖。

所以我們在使用鎖的時候要特別注意hashcode生成給鎖升級帶來的影響。

  • 輕量級鎖狀態(tài)

當(dāng)?shù)诙€線程T2進(jìn)入代碼塊后

  1. 判斷是否處于偏向中(通過Mark Word中的是否偏向判斷)
  2. 如果處于偏向中,T2會以cas的方式試圖將Mark Word中的線程id替換為自己的線程id
  3. 如果T1已經(jīng)執(zhí)行完代碼塊,T2一定是可以替換成功的,此時鎖依然是偏向鎖狀態(tài)
  4. 如果T1沒有執(zhí)行完代碼塊,T2一定是替換不成功的,此時將進(jìn)入偏向鎖撤銷升級為輕量級鎖的過程
  5. 首先T1會進(jìn)入到安全點(diǎn),T1和T2會在自己的??臻g開辟一塊區(qū)域用于保存鎖記錄,同時復(fù)制一份Mark Word到這個鎖記錄中,同時cas的方式將自己??臻g這個鎖記錄的指針設(shè)置到Mark Word中去,因?yàn)門1持有偏向鎖,所以T1會優(yōu)先設(shè)置成功,此時Mark Word中有30bit的鎖記錄指針和2bit的鎖標(biāo)志位,此時的鎖標(biāo)志位為00代表輕量級鎖,鎖記錄指針指向當(dāng)前持有輕量級鎖的線程中??臻g的地址。T2沒有替換成功,將會進(jìn)入不斷輪詢失敗重試過程。

輕量級鎖是在資源競爭壓力不是很大的情況下,避免每個線程都去獲取鎖而造成用戶態(tài)到內(nèi)核態(tài)的切換,這個切換是比較耗時的,這樣就能提高性能,但是如果競爭壓力大的情況下輕量級鎖就不行了,因?yàn)閴毫Υ笠馕吨泻芏嗑€程在輪序失敗重試獲取輕量級鎖,短時間內(nèi)會造成cpu壓力飆升,甚至拖垮cpu,這個時候就必須升級為重量級鎖。

那么如何才算競爭壓力大,什么時候會升級為重量級鎖呢?

jvm默認(rèn)輪詢次數(shù)限制值為十次,超過十次獲取不到資源就代表競爭壓力比較大了,用戶也可以使用如下參數(shù)配置來自行更改這個次數(shù)

-XX:PreBlockSpin

但是有個問題,如果通過這個默認(rèn)值或者這個jvm參數(shù)配置限制數(shù)量,那意味著jvm全系統(tǒng)的鎖都要遵循,這個數(shù)字可能不適用于所有的鎖,因此jvm引入了自適應(yīng)的自旋。自適應(yīng)意味著自旋的時間不再是固定的了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對更長的時間,比如持續(xù)100次忙循環(huán)。另一方面,如果對于某個鎖,自旋很少成功獲得過鎖,那在以后要獲取這個鎖時將有可能直接省略掉自旋過程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,隨著程序運(yùn)行時間的增長及性能監(jiān)控信息的不斷完善,虛擬機(jī)對程序鎖的狀況預(yù)測就會越來越精準(zhǔn),虛擬機(jī)就會變得越來越“聰明”了。

4.重量級鎖

重量級鎖就是Monitor鎖,也叫監(jiān)視器鎖,其實(shí)現(xiàn)是依靠操作底層的互斥原語Mutes Lock,因?yàn)槊恳淮潍@取Monitor鎖都需要用戶態(tài)到內(nèi)核態(tài)的切換,所以比較耗時,也就是重量級鎖的由來。當(dāng)自旋的條件破壞后,比如自旋次數(shù)達(dá)到限制或者競爭的壓力越來越大,將不再自旋,輕量級鎖升級為重量級鎖,當(dāng)前對象頭中的Mark Word被復(fù)制一份到Monitor對象中,Mark Word中原來的輕量級鎖的鎖記錄指針被換成Monitor對象的指針,然后所有的線程會搶奪Monitor鎖的擁有權(quán),以cas方式將自己的線程id填充到Monitor對象的_owner字段,同時_count字段加1,當(dāng)然此時能夠cas成功的只會是原來持有輕量級鎖的線程,而那些沒有獲取到Monitor鎖的線程將會被阻塞并放入Monitor對象的_EntryList字段等待喚醒。

此時鎖的標(biāo)志位為10,表示重量級鎖。

當(dāng)線程退出Monitor鎖,便會將Monitor鎖中的_count減1,清空_owner,jvm會隨機(jī)喚醒_EntryList集合中一個線程重新獲取Monitor鎖,這個隨機(jī)便突出了synchronized的不公平性。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)本農(nóng)
相關(guān)推薦

2023-10-11 08:22:33

線程AQScondition

2023-10-09 08:04:52

面試CASABA

2023-10-17 15:56:37

FutureTask線程

2023-10-16 10:09:41

線程進(jìn)程

2023-10-10 08:55:12

AQS阻塞

2022-04-13 14:43:05

JVM同步鎖Monitor 監(jiān)視

2022-04-11 07:40:45

synchroniz靜態(tài)方法程序

2021-07-04 08:01:30

Synchronize線程安全并發(fā)編程

2021-12-16 18:38:13

面試Synchronize

2022-04-24 07:59:53

synchronizJVMAPI

2023-02-01 07:15:16

2020-12-01 07:16:05

重學(xué)設(shè)計模式

2022-12-26 09:27:48

Java底層monitor

2022-07-06 07:35:19

group byMySQL

2022-06-10 13:56:42

Java

2023-10-26 16:02:04

線程

2024-01-11 08:12:20

重量級監(jiān)視器

2022-09-12 22:27:05

編程式事務(wù)聲明式事務(wù)對象

2012-08-08 10:00:17

面試技術(shù)

2022-02-21 08:20:24

Wait方法Notify
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號