溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

sychronized關(guān)鍵字的作用是什么

發(fā)布時間:2021-07-23 15:46:08 來源:億速云 閱讀:211 作者:Leah 欄目:編程語言

本篇文章給大家分享的是有關(guān)sychronized關(guān)鍵字的作用是什么,小編覺得挺實用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

sychronized用法

修飾代碼段

public class Test {
	Object lock = new Object();
	int i = 0;
	public void f() {
		sychronized (lock) {
			i++;
		}
	}
}

適用于方法體比較大或者耗時,但需要同步的代碼塊比較短的場景。

修飾非靜態(tài)類方法

public class Test {
	
	int i = 0;
	public sychronized void f() {
		i++;
	}
}

當(dāng)sychronized關(guān)鍵字修飾一個非靜態(tài)方法的時候,其鎖對象是所修飾方法所屬的實例。也就是說若a實例有m1和m2兩個方法都被sychronized關(guān)鍵字修飾,那么他們是沒法在多個線程中同時執(zhí)行的,存在對a實例的鎖競爭。

修飾靜態(tài)方法

public class Test {
	public sychronized void f() {
		System.out.println("test sychronized");
	}
}

當(dāng)sychronized修飾靜態(tài)方法是,其鎖對象是所修飾方法所屬的class對象。

sychronized原理

對sychronized支持的實現(xiàn)是在JVM層面的,每個Java對象都存在一個叫做對象監(jiān)視器的結(jié)構(gòu),而同步過程就是依賴于對這個同步監(jiān)視器的持有權(quán)的競爭來實現(xiàn)的。下面介紹一個概念————對象頭。

Java對象頭

每個Java對象在JVM中都分為三塊區(qū)域:對象頭,實例數(shù)據(jù)和填充對齊。

  • 實例數(shù)據(jù):存放類及其父類的屬性信息

  • 填充對齊:由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。所以不滿整數(shù)倍的會有一些額外的空間來補(bǔ)齊,類似于C語言中的結(jié)構(gòu)體。

而對象頭則是存儲了一些Java對象的額外信息,主要包括一些運(yùn)行時數(shù)據(jù)(Mark Word)、類型指針、若對象為數(shù)組,則還包括數(shù)組長度。運(yùn)行時數(shù)據(jù)有:hashcode、GC分代年齡、鎖狀態(tài)標(biāo)識、以及根據(jù)不同鎖的類型,該結(jié)構(gòu)的內(nèi)容也會有一些變化。sychronized用的鎖就是存在Java對象頭里的。在64位虛擬機(jī)下,Mark Word是64bit。不同鎖的狀態(tài)下,其結(jié)構(gòu)如下:

  • 無鎖:(25bit)Unused + (31bit)HashCode + (1bit)cms_free + (4bit)分代年齡 + (1bit)0 + (2bit) 鎖標(biāo)志位01

  • 偏向鎖:(54bit)ThreadID + (2bit)Epoch + (1bit)cms_free + (4bit)分代年齡 + (1bit)1 + (2bit) 鎖標(biāo)志位01

  • 輕量級鎖:(62bit) ptr_to_lock_record + (2bit) 鎖標(biāo)志位00

  • 重量級鎖:(62bit) ptr_to_heavyweight_monitor + (2bit) 鎖標(biāo)志位10

還有一種GC情況下,其結(jié)構(gòu)為:(62bit) Unused + (2bit) GC標(biāo)記11

ObjectMonitor

這里我們先討論重量級鎖的情況,也就是sychronized常說的對象鎖,此時Mark Word中的前62bit是一個指向重量級鎖對象的指針,sychronized在JVM中是通過monitorenter和monitorexit指令來實現(xiàn)的,在底層則是通過爭奪重量級鎖對象的方式來實現(xiàn)方法同步和代碼塊同步。鎖對象被定義為ObjectMonitor,其結(jié)構(gòu)如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 獲取鎖的次數(shù)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 當(dāng)前持有鎖的線程
    _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 ;
  }

ObjectMonitor中有兩個隊列,_WaitSet_EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象 )

當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1。

若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入 WaitSet集合中等待被喚醒。

若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。

monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因。

synchronized代碼塊底層原理

同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置。

當(dāng)執(zhí)行monitorenter指令時,當(dāng)前線程將試圖獲取對象鎖的持有權(quán),當(dāng)_count為0時,那線程可以成功取得對象鎖,并將計數(shù)器值設(shè)置為1,取鎖成功。如果當(dāng)前線程已經(jīng)擁有某一對象鎖,那它可以重入這個鎖,重入時計數(shù)器的值也會加1。倘若其他線程已經(jīng)擁有對象鎖的所有權(quán),則當(dāng)前線程阻塞,直到正在執(zhí)行的線程執(zhí)行monitorexit指令完畢,執(zhí)行線程將釋放鎖并設(shè)置計數(shù)器值為0,其他線程將有機(jī)會持有鎖。

synchronized方法底層原理

方法級的同步是隱式,即無需通過字節(jié)碼指令來控制的,它實現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否同步方法。

當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。

鎖升級過程

早起的Java版本中,synchronized屬于重量級鎖,效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized效率低的原因。

Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖。鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級。

偏向鎖

偏向鎖是JDK6中的重要引進(jìn),因為HotSpot作者經(jīng)過研究實踐發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進(jìn)了偏向鎖。

偏向鎖是在單線程執(zhí)行代碼塊時使用的機(jī)制,如果在多線程并發(fā)的環(huán)境下(即線程A尚未執(zhí)行完同步代碼塊,線程B發(fā)起了申請鎖的申請),則一定會轉(zhuǎn)化為輕量級鎖或者重量級鎖。

在JDK5中偏向鎖默認(rèn)是關(guān)閉的,而到了JDK6中偏向鎖已經(jīng)默認(rèn)開啟。如果并發(fā)數(shù)較大同時同步代碼塊執(zhí)行時間較長,則被多個線程同時訪問的概率就很大,就可以使用參數(shù)-XX:-UseBiasedLocking來禁止偏向鎖(但這是個JVM參數(shù),不能針對某個對象鎖來單獨(dú)設(shè)置)。

引入偏向鎖主要目的是:為了在沒有多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑。因為輕量級鎖的加鎖解鎖操作是需要依賴多次CAS原子指令的,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗也必須小于節(jié)省下來的CAS原子指令的性能消耗)。

輕量級鎖是為了在線程交替執(zhí)行同步塊時提高性能,而偏向鎖則是在只有一個線程執(zhí)行同步塊時進(jìn)一步提高性能。

當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程進(jìn)入和退出同步塊時不需要花費(fèi)CAS操作來爭奪鎖資源,只需要檢查是否為偏向鎖、鎖標(biāo)識為以及ThreadID即可。

偏向鎖的釋放采用了 一種只有競爭才會釋放鎖的機(jī)制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。

sychronized關(guān)鍵字的作用是什么

輕量級鎖

引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。其適用場景為線程交替執(zhí)行同步塊的情況。當(dāng)關(guān)閉偏向鎖功能或者多個線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖。其步驟如下:

  1. 在線程進(jìn)入同步塊時,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word;

  2. 拷貝對象頭中的Mark Word復(fù)制到鎖記錄(Lock Record)中;

  3. 拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對象Mark Word中的Lock Word更新為指向當(dāng)前線程Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟(4),否則執(zhí)行步驟(5);

  4. 如果這個更新動作成功了,那么當(dāng)前線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài);

  5. 如果這個更新操作失敗了,虛擬機(jī)首先會檢查對象Mark Word中的Lock Word是否指向當(dāng)前線程的棧幀,如果是,就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,進(jìn)入自旋執(zhí)行(3),若自旋結(jié)束時仍未獲得鎖,輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當(dāng)前線程以及后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。

對于輕量級鎖,其性能提升的依據(jù)是 “對于絕大部分的鎖,在整個生命周期內(nèi)都是不會存在競爭的”,如果打破這個依據(jù)則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢。

sychronized關(guān)鍵字的作用是什么

自旋鎖

輕量級鎖失敗后,虛擬機(jī)為了避免線程真實地在操作系統(tǒng)層面掛起,還會進(jìn)行一項稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會讓當(dāng)前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因),一般不會太久,可能是50個循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。

鎖消除

消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時進(jìn)行編譯,又稱即時編譯),通過對運(yùn)行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間。

鎖的比較

sychronized關(guān)鍵字的作用是什么

ReetrantLock區(qū)別

  • sychronized是在JVM層面提供的支持,而Lock接口的系列實現(xiàn)則是在jdk層面和操作系統(tǒng)層面。

  • sychronized的實現(xiàn)涉及到鎖的升級,而ReetrantLock的實現(xiàn)則是通過AQS結(jié)構(gòu),CAS保證原子性,volatile保證可見性

  • synchronized 不需要用戶去手動釋放鎖,synchronized 代碼執(zhí)行完后系統(tǒng)會自動讓線程釋放對鎖的占用; ReentrantLock則需要用戶去手動釋放鎖,如果沒有手動釋放鎖,就可能導(dǎo)致死鎖現(xiàn)象。一般通過lock()和unlock()方法配合try/finally語句塊來完成,使釋放更加靈活。

  • synchronized是不可中斷類型的鎖,除非加鎖的代碼中出現(xiàn)異常或正常執(zhí)行完成; ReentrantLock則可以中斷,可通過trylock(long timeout,TimeUnit unit)設(shè)置超時方法或者將lockInterruptibly()放到代碼塊中,調(diào)用interrupt方法進(jìn)行中斷。

  • synchronized為非公平鎖 ReentrantLock則即可以選公平鎖也可以選非公平鎖,通過構(gòu)造方法new ReentrantLock時傳入boolean值進(jìn)行選擇,為空默認(rèn)false非公平鎖,true為公平鎖。

  • synchronized不能綁定; ReentrantLock通過綁定Condition結(jié)合await()/singal()方法實現(xiàn)線程的精確喚醒,而不是像synchronized通過Object類的wait()/notify()/notifyAll()方法要么隨機(jī)喚醒一個線程要么喚醒全部線程。

  • synchronzied鎖的是對象,鎖是保存在對象頭里面的,根據(jù)對象頭數(shù)據(jù)來標(biāo)識是否有線程獲得鎖/爭搶鎖;ReentrantLock鎖的是線程,根據(jù)進(jìn)入的線程和int類型的state標(biāo)識鎖的獲得/爭搶。

其他

可重入性

從互斥鎖的設(shè)計上來說,當(dāng)一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態(tài),但當(dāng)一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內(nèi)部鎖機(jī)制,是可重入的,因此在一個線程調(diào)用synchronized方法的同時在其方法體內(nèi)部調(diào)用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。

線程中斷

當(dāng)一個線程處于被阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態(tài)將會被復(fù)位(由中斷狀態(tài)改為非中斷狀態(tài))。

但是,當(dāng)線程處于運(yùn)行期且是非阻塞狀態(tài),直接調(diào)用interrupt()方法中斷線程,是不會得到任何響應(yīng)的。

線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用,也就是對于synchronized來說,如果一個線程在等待鎖,那么結(jié)果只有兩種,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保存等待,即使調(diào)用中斷線程的方法,也不會生效。

等待喚醒機(jī)制

每個對象都有notify/notifyAll和wait這三個頂級方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調(diào)用這幾個方法前必須拿到當(dāng)前對象的監(jiān)視器monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道m(xù)onitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關(guān)鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因。

以上就是sychronized關(guān)鍵字的作用是什么,小編相信有部分知識點(diǎn)可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI