溫馨提示×

溫馨提示×

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

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

如何理解程序編寫中的鎖

發(fā)布時間:2021-10-12 09:49:50 來源:億速云 閱讀:149 作者:iii 欄目:編程語言

這篇文章主要講解了“如何理解程序編寫中的鎖”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解程序編寫中的鎖”吧!

如何理解程序編寫中的鎖

 鎖到底是一種怎樣的存在?

隨著業(yè)務的發(fā)展與用戶量的增加,高并發(fā)問題往往成為程序員不得不面對與處理的一個很棘手的問題,而并發(fā)編程又是編程領域相對高級與晦澀的知識,想要學好并發(fā)相關的知識,寫出好的并發(fā)程序不是那么容易的。

對于寫Java的程序員說,在這一點上可能要相對幸福一些,因為Java中存在大量的封裝好的同步原語以及大師編寫的同步工具類,使得編寫正確且高效的并發(fā)程序的門檻降低了許多。

這種高度的封裝抽象雖然簡化了程序的書寫,卻對我們了解其內(nèi)部實現(xiàn)機制產(chǎn)生了一定的阻礙,現(xiàn)在就讓我們從現(xiàn)實世界中的鎖的角度進行類比,看看程序世界中的鎖到底是一種怎樣的存在?

程序世界中的鎖

如果有人問你:"如何確保房屋不被陌生人進入"?我想你可能很容易想到:“上鎖就可以了嘛!”。而如果有人問你:"如何處理多個線程的并發(fā)問題"?我想你可能脫口而出:"加鎖就可以了嘛!"。

類似的場景在現(xiàn)實世界中很容易理解,但是在程序世界中,這幾個字卻充滿了疑惑。我們見過現(xiàn)實世界中各種各樣的鎖,那Java中的鎖長什么樣子?

我們現(xiàn)實世界中通常需要鑰匙打開鎖進入房屋,那打開程序世界中的鎖的那把鑰匙是什么?現(xiàn)實中鎖通常位于門上或者櫥柜上或者其他位置,那程序世界中的鎖存在于哪里呢?現(xiàn)實世界中上鎖開鎖的通常是我們?nèi)?,那程序世界中加鎖解鎖的又是誰呢?

帶著這些疑問,我們想要深入的了解一下Java中鎖到底是一種怎樣的存在?從哪里開始了解呢,我想鎖在程序中首先是被用來使用的,那就先從鎖的使用開始偵查吧!

鎖的使用

提到 Java中的鎖,通??梢苑譃閮深?,一類是JVM級別提供的并發(fā)同步原語Synchronized, 另一類就是 Java API級別的Lock接口的那些若干實現(xiàn)類。

Java API級別的鎖比如Reentrantlock和ReentrantReadWriteLock等這些存在很詳細的源碼,大家可以去看看他們是怎么實現(xiàn)的,也許可以尋找到上面的答案,這里我們看一下Synchronized。

先來看下面這段代碼:

public class LockTest  {      Object obj=new Object();      public static synchronized void testMethod1()      {          //同步代碼。      }      public synchronized void testMethod2()      {          //同步代碼      }      public void testMethod3()      {          synchronized (obj)          {              //同步代碼          }      }  }

很多并發(fā)編程書籍對于Synchronized的用法都做了如下總結(jié):

  •  Synchronized修飾靜態(tài)方法的時候(對應testMethod1),鎖的是當前類的class對象,對應到這里就是LockTest.class_對象_。

  •  Synchronized修飾實例方法的時候(對應testMethod2),鎖的是當前類實例的對象,對應到這里就是LocKTest中的this引用_對象_。

  •  Synchronized修飾同步代碼塊的時候(對應testMethod3),鎖的是同步代碼塊括號里的對象實例,對應到這里就是obj_對象_。

從這里我們可以看到,Synchronized的使用都要依賴特定的對象,從這里可以發(fā)現(xiàn)鎖與對象存在某種關聯(lián)。那么我們下一步看看對象中到底有什么關于鎖的蛛絲馬跡。

對象的組成

Java中一切皆對象,就好比你的對象有長長的頭發(fā),大大的眼睛(或許一切只是想象)... Java中的對象由三部分組成。分別是對象頭、實例數(shù)據(jù)、對齊填充。

實例數(shù)據(jù)很好理解,就是我們在類中定義的那些字段數(shù)據(jù)所占用的空間。而對齊填充呢是因為Java特定的虛擬機要求對象的大小必須是8字節(jié)的整數(shù)倍,如果一個對象鎖占用的存儲空間最后會有一個不夠8字節(jié)的碎片,那么要把他填充到8字節(jié)??雌饋礞i與這兩個區(qū)域都不會有太大的關系,那么鎖應該與對象頭存在某種關系,如下圖:

如何理解程序編寫中的鎖

對象組成.png

下面來看一下對象頭中的內(nèi)容:

如何理解程序編寫中的鎖

我們以32位虛擬機為例(64位的類比即可),Mark Word只有四個字節(jié),而且還要存放HashCode等信息,難道鎖就完全存在于這四個字節(jié)之內(nèi)就可以實現(xiàn)嘛?這句話在Jdk1.6之前是完全不對的,在Jdk1.6之后在一部分情況下是對的。

為什么這么說呢?

這是因為Java中的線程是與本地的操作系統(tǒng)線程一一對應的,而操作系統(tǒng)為了保護系統(tǒng)內(nèi)部的安全,防止一些內(nèi)部指令等的隨意調(diào)用,保證內(nèi)核的安全,將系統(tǒng)空間分為了用戶態(tài)與內(nèi)核態(tài),我們平時所運行的線程只是運行在用戶態(tài),當我們需要調(diào)用操作系統(tǒng)服務(這里被稱為系統(tǒng)調(diào)用),比如read,writer等操作時,是沒有辦法在用戶態(tài)直接發(fā)起調(diào)用的,這個時候就需要進行用戶態(tài)與內(nèi)核態(tài)的切換。

而Synchronized早期被稱為重量級鎖的原因是因為使用Synchronized所進行的加鎖與解鎖都要進行用戶態(tài)與內(nèi)核態(tài)的切換,所以早期的Synchronized是重量級鎖,需要實現(xiàn)線程的阻塞與喚醒,阻塞隊列與條件隊列的出隊與入隊等等,這些我們后面再說,顯然是不可能存放在這四個字節(jié)之內(nèi)的。但是Jdk1.6時對Synchronized進行了一系列優(yōu)化,其中就包括了鎖升級,使得這句話變得部分對了。

鎖升級的過程

之所以說前面那句話在部分情況下是正確的,是因為在Jdk1.6時,虛擬機團隊對Synchronized進行了一系列的優(yōu)化,具體我們就不討論了,很多的并發(fā)編程書籍中都有詳細的記錄。而這里我們要說的就是其中的一項重要的優(yōu)化——鎖升級。

Java中Synchronized的鎖升級過程如下:無鎖——>偏向鎖——>輕量級鎖——>重量級互斥鎖。

也就是說除非存在很嚴重的多線程之間的鎖競爭,否則Synchronized不會使用Jdk1.6之前那么重的互斥鎖了。

我們知道現(xiàn)實世界中是由我們?nèi)藖碡撠熯M行上鎖和開鎖的,那么程序世界中其實是由線程來扮演人的角色來進行加鎖解鎖的。

偏向鎖

剛開始的時候,處于無鎖狀態(tài),我們可以理解為寶屋的門沒鎖著,這時第一個線程運行到了同步代碼區(qū)域(第一個人走到了門前),加上了一個偏向鎖,這個時候鎖是一種什么形態(tài)呢?這個時候其實是類似一種人臉識別鎖的形態(tài),第一個進入同步代碼塊的線程自身作為鑰匙,將能夠唯一標識一個線程的線程ID保存到了Mark Word中。

這個時候的Mark Word中的內(nèi)容如下:

如何理解程序編寫中的鎖

偏向鎖.jpg

這里的四個字節(jié)的23位用來存儲第一個獲取偏向鎖的線程的線程ID,2位的Epoch代表偏向鎖的有效性,4位對象分代年齡,1位是否是偏向鎖(1為是),2位鎖標志位(01是偏向鎖)。

當?shù)谝粋€線程運行到同步代碼塊的時候,會去檢查Synchronized鎖使用的那個對象的對象頭,如果上面所談的Synchronized所使用的三種對象其中之一的對象頭的線程ID這個地方為空的話,并且偏向鎖是有效的,說明當前還是處于無鎖的狀態(tài)(也就是寶屋還沒有上鎖),那么這個時候第一個線程就會使用CAS的方式將自己的線程ID替換到對象頭Mark Word的線程ID,如果替換成功說明該線程獲取到了偏向鎖,那么線程就可以安全的執(zhí)行同步代碼了,以后如果線程再次進入同步代碼的時候,在此期間如果其他線程沒有獲取偏向鎖,只需要簡單的對比一下自己的線程ID與Mark Word中的線程ID是否一致,如果一致就可以直接進入同步代碼區(qū)域,這樣性能損耗就小多了。

偏向鎖是基于這樣的一個事實,HotSpot的研發(fā)團隊曾經(jīng)做個一個研究并表明,通常情況下鎖并不會發(fā)生競爭,并且總是由同一個線程多次的獲取鎖,在這種情況下引入偏向鎖可以說好處大大的了!

相反如果這種情況不是很常見的話,也就是說鎖的競爭很嚴重,或者通常情況下鎖是由多個線程輪流獲取,這樣子偏向鎖就沒什么用處了。

輕量級鎖

從這里我們可以看出,當鎖開始時是偏向鎖的時候是以一種怎樣的形態(tài)存在,前面我們也說了偏向鎖是在不存在多個線程競爭鎖的情況下存在的,然而高并發(fā)環(huán)境下競爭鎖是不可避免的,此時Synchronized便開啟了他的晉升之路。

當存在多個線程競爭鎖的時候,這時候簡單的偏向鎖就不是那么安全了,鎖不住了,這時就要換鎖,升級成一種更為安全的鎖。此時的鎖升級過程大概可以分為兩步:(1)偏向鎖的撤銷(2)輕量級鎖的升級。

首先偏向鎖如何撤銷呢,我們說偏向鎖的鎖其實就是Mark Work中的線程ID,這個時候只要更改Mark Word自然就相當于撤銷了偏向鎖,那么問題是偏向鎖用線程ID表示,輕量級鎖該用什么表示呢?答案是Lock Record(棧楨中的鎖記錄)。

這里我來解釋一下:

我們知道JVM內(nèi)存結(jié)構(gòu)可以分為(1)堆(2)虛擬機棧(3)本地方法棧(4)程序計數(shù)器(5)方法區(qū)(6)直接內(nèi)存。這其中程序計數(shù)器和虛擬機棧是線程私有的啊,每個線程都擁有自己獨立的??臻g,看起來存放在棧中可以很好的區(qū)分開是哪個線程獲取到了鎖,事實上,JVM也確實是這么做的。

首先,JVM會在當前的棧中開辟一塊內(nèi)存,這塊內(nèi)存被稱為Lock Record(鎖記錄),并把Mark Word中的內(nèi)容復制到Lock Record中(也就是說Lock Record中存放的是之前的Mark Work中的內(nèi)容,那為什么要存之前的內(nèi)容呢?

很簡單,因為我們馬上就要修改Mark Word的內(nèi)容了,修改之前當然要保存一下,以便日后恢復啊),復制完了之后接下來就要開始修改Mark Word了,如何修改呢?當然是用CAS的方式替換Mark Word了!此時Mark Word將變成以下內(nèi)容:

如何理解程序編寫中的鎖

輕量級鎖.jpg

可以看到Mark Word中使用30位來記錄我們剛剛在棧楨中創(chuàng)建的Lock Record,鎖標志位為00表示輕量級鎖,這樣就很容易知道是哪個線程獲取到了輕量級鎖啦。

輕量級鎖是基于這樣的一個事實,當存在兩個或以上的線程競爭鎖的時候,絕大多數(shù)情況下,持有鎖的線程是會很快釋放鎖的,也就是當鎖存在少量競爭時,通常情況下鎖被持有的時間很短,此時等待獲取鎖的線程可以不必進行用戶態(tài)與內(nèi)核態(tài)的切換從而阻塞自己,而只要空循環(huán)(這個叫自旋)一會兒,期望在自旋的這段時候持有鎖的線程可以馬上釋放掉鎖。

很明顯輕量級鎖適用于鎖的競爭并不激烈并且鎖被持有的時間很短的情況,相反如果鎖競爭激烈或者線程獲取到鎖之后長時間不釋放鎖,那么線程會白白的自旋(死循環(huán))而浪費掉cpu資源。

重量級互斥鎖

當想要進入寶屋的人太多時,輕量級也不行了,這個時候只能使用殺手锏了——重量級互斥鎖。這也是Synchronized在Jdk1.6之前的默認實現(xiàn)。

當鎖處于輕量級鎖的時候,線程需要自旋等待持有鎖的線程釋放鎖,然后去申請鎖,但是存在兩個問題:

  1.  自旋的線程很多,也就是有很多線程都在等待當前持有鎖的線程釋放鎖,由于鎖只能同一時刻被一個線程獲?。ň蚐ynchronized而言),這樣就導致大量的線程獲取鎖失敗,總不能一直的自旋下去吧?

  2.  持有鎖的線程長時間不釋放鎖,導致在外面等待獲取鎖的線程長時間自旋仍然獲取不到鎖,總不能一直自旋下去吧?

上述兩種情況下分別來看,等待獲取鎖的線程就很難受了,如果兩種情況同時滿足(鎖競爭激烈同時持有鎖的線程長時間不釋放鎖),那就更難受了。于是JVM設定了一個自旋次數(shù)的限制,如果線程自旋了一定的次數(shù)之后仍然沒有獲取到鎖,那么可以視為鎖競爭比較激烈的情況了,這個時候線程請求撤銷輕量級鎖,晉升為重量級的互斥鎖。

在輕量級鎖的時候,鎖是以Lock Record的形式存在的,那么到了重量級鎖的時候,該以什么形式存在呢?

重量級鎖的復雜度是最高的,由于持有鎖的線程在釋放鎖時候需要喚醒阻塞等待的線程,線程獲取不到鎖的時候需要進入某一個阻塞區(qū)域統(tǒng)一阻塞等待,同時我們知道還有wait,notify條件的等待與喚醒需要處理,所以重量級鎖的實現(xiàn)需要一個額外的大殺器——Monitor。

在《Java并發(fā)編程的藝術(shù)》一書中有著這樣的描述:

JVM基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,但兩者的實現(xiàn)細節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現(xiàn)的,而方法同步是使用另外一種方式實現(xiàn)的,細節(jié)在JVM規(guī)范里并沒有 詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現(xiàn)。

monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯(lián),當且一個monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權(quán),即嘗試獲得對象的鎖。

我們以HotSpot虛擬機為例,其是用C++實現(xiàn)的,C++也是一門面向?qū)ο蟮恼Z言,因此,虛擬機設計團隊這一次選擇以對象的形態(tài)表示鎖,同時C++也支持多態(tài),這里的Monitor其實是一種抽象,虛擬機中對于Monitor的實現(xiàn)使用ObjectMonitor實現(xiàn),關于Monitor與ObjectMonitor的關系可以類比Java中Map與HashMap的關系。

我們看一下ObjectMonitor的真容:

ObjectMonitor()     {      _header       = NULL;      _count        = 0;//用來記錄該線程獲取鎖的次數(shù)      _waiters      = 0,      _recursions   = 0;//鎖的重入次數(shù)      _object       = NULL;      _owner        = NULL;//指向持有ObjectMonitor的線程      _WaitSet      = NULL;//存放處于Wait狀態(tài)的線程的集合      _WaitSetLock  = 0 ;      _Responsible  = NULL ;      _succ         = NULL ;      _cxq          = NULL ;      FreeNext      = NULL ;      _EntryList    = NULL ;//所以等待獲取鎖而被阻塞的線程的集合      _SpinFreq     = 0 ;      _SpinClock    = 0 ;      OwnerIsThread = 0 ;    }

這里強烈建議大家去看一下基于AQS(抽象隊列同步器)實現(xiàn)的ReentrantLock的實現(xiàn)源碼,因為ReentrantLock內(nèi)部的同步器實現(xiàn)思路基本上就是Synchronized實現(xiàn)中的Monitor的縮影。

首先ObjectMonitor中需要有一個指針指向當前獲取鎖的線程,就是上面的owner,當某一個線程獲取鎖的時候,將調(diào)用ObjectMonitor.enter()方法進入同步代碼塊,獲取到鎖之后,就將owner設置為指向當前線程,當其他的線程嘗試獲取鎖的時候,就找到ObjectMonitor中的owner看看是否是自己,如果是的話,recursions和count自增1,代表該線程再次的獲取到了鎖(Synchronized是可重入鎖,持有鎖的線程可以再次的獲取鎖),否則的話就應該阻塞起來,那么這些阻塞的線程放在哪里呢?

統(tǒng)一的放在EntryList中即可。當持有鎖的線程調(diào)用wait方法時(我們知道wait方法會使得線程放棄cpu,并釋放自己持有的鎖,然后阻塞掛起自己,直到其他的線程調(diào)用了notify或者notifyAll方法為止),那么線程應該釋放掉鎖,把owner置為空,并喚醒EntryList中阻塞等待獲取鎖的線程,然后將自己掛起并進入waitSet集合中等待,當其他持有鎖的線程調(diào)用了notify或者或者notifyAll方法時,會將WaitSet中的某一個線程(notify)或者全部線程(notifyAll)從WaitSet中移動到EntryList中等待競爭鎖,當線程要釋放鎖的時候,就會調(diào)用ObjectMonitor.exit()方法退出同步代碼塊。結(jié)合《Java并發(fā)編程的藝術(shù)》中的描述,一切都很清晰了。

鎖升級為重量級鎖同樣需要兩個步驟:(1)輕量級鎖的撤銷(2)重量級鎖升級。

要撤銷輕量級鎖,當然要把保存在棧楨中的Lock Record中存儲的內(nèi)容再寫回Mark Work中,然后將棧楨中的Lock Record清理掉。此后需要創(chuàng)建一個ObjectMonitor對象,并且將Mark Word中的內(nèi)容保存到ObjectMonitor中(便于撤銷鎖的時候恢復Mark Word,這里是保存在了ObjectMonitor中)。那么如何尋找到這個ObjectMonitor對象呢?哈哈沒錯就是在Mark Word中記錄指向ObjectMonitor對象的指針即可。如何修改替換Mark Word中的內(nèi)容呢?當然會CAS啦!

鎖在重量級互斥鎖的形態(tài)下Mark Word中的內(nèi)容如下:

如何理解程序編寫中的鎖

重量級鎖.jpg

可以看到Mark Word中使用30位來保存指向ObjectMonitor的指針,鎖標記位為10,表示重量級鎖。

重量級鎖基于這樣的一個事實,當鎖存在嚴重的競爭,或者鎖持有的時間通常很長的時候,等待獲取鎖的線程應該阻塞掛起自身,等待獲得鎖的線程釋放鎖的時候的喚醒,這樣避免白白的浪費cpu資源。

鎖形態(tài)的變遷

現(xiàn)在我們可以回答文章開頭“ Java中的鎖長什么樣子?”這個問題了,在不同的鎖狀態(tài)下,鎖表現(xiàn)出了不同的形態(tài)。

當鎖以偏向鎖存在的時候,鎖就是Mark Word中的Thread ID,此時線程本身就是打開鎖的鑰匙,Mark Word中存了哪個線程的"身份證",哪個線程就獲得了鎖。

當鎖以輕量級鎖存在的時候,鎖就是Mark Word中所指向棧楨中鎖記錄的Lock Record,此時的鑰匙就是地盤,是虛擬機棧,誰的棧中有Lock Record,誰就獲得了鎖。

當鎖以重量級鎖存在的時候,鎖就是C++中對于Monitor的實現(xiàn)ObjectMonitor,此時的鑰匙就是ObjectMonitor中的owner。owner指向誰,誰就獲得了鎖。

之前的問題中,我們說32位的虛擬機Mark Word只有四個字節(jié),難道鎖就完全存在于這四個字節(jié)之內(nèi)就可以實現(xiàn)嘛?這句話在Jdk1.6之前是完全不對的,在Jdk1.6之后在一部分情況下是對的?,F(xiàn)在你是否對這句話有了更深刻的理解呢?

而現(xiàn)實世界中上鎖開鎖的是我們?nèi)祟?,通過前面的了解,程序世界中上鎖開鎖的又是誰呢?是的就是線程了。

現(xiàn)在再回頭看文章開頭的那些問題,就很容易給出答案了,原來一切真的就是從Synchronized使用的那個鎖對象開始的!

感謝各位的閱讀,以上就是“如何理解程序編寫中的鎖”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對如何理解程序編寫中的鎖這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

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

AI