91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

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

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

如何理解程序編寫中的鎖

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

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

如何理解程序編寫中的鎖

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

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

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

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

程序世界中的鎖

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

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

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

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

鎖的使用

提到 Java中的鎖,通常可以分為兩類,一類是JVM級別提供的并發同步原語Synchronized, 另一類就是 Java API級別的Lock接口的那些若干實現類。

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

先來看下面這段代碼:

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

很多并發編程書籍對于Synchronized的用法都做了如下總結:

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

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

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

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

對象的組成

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

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

如何理解程序編寫中的鎖

對象組成.png

下面來看一下對象頭中的內容:

如何理解程序編寫中的鎖

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

為什么這么說呢?

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

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

鎖升級的過程

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

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

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

我們知道現實世界中是由我們人來負責進行上鎖和開鎖的,那么程序世界中其實是由線程來扮演人的角色來進行加鎖解鎖的。

偏向鎖

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

這個時候的Mark Word中的內容如下:

如何理解程序編寫中的鎖

偏向鎖.jpg

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

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

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

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

輕量級鎖

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

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

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

這里我來解釋一下:

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

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

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

如何理解程序編寫中的鎖

輕量級鎖.jpg

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

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

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

重量級互斥鎖

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

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

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

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

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

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

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

在《Java并發編程的藝術》一書中有著這樣的描述:

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

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

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

我們看一下ObjectMonitor的真容:

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

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

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

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

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

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

鎖在重量級互斥鎖的形態下Mark Word中的內容如下:

如何理解程序編寫中的鎖

重量級鎖.jpg

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

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

鎖形態的變遷

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

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

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

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

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

而現實世界中上鎖開鎖的是我們人類,通過前面的了解,程序世界中上鎖開鎖的又是誰呢?是的就是線程了。

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

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

向AI問一下細節

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

AI

华阴市| 麦盖提县| 安义县| 湖南省| 拉孜县| 陵川县| 民和| 丰宁| 连平县| 惠安县| 新竹县| 沈丘县| 贡山| 邵阳县| 独山县| 长春市| 万州区| 澳门| 西华县| 犍为县| 武宣县| 东乡| 乐东| 时尚| 隆林| 河西区| 方山县| 二手房| 神池县| 页游| 保康县| 大邑县| 双城市| 枞阳县| 鹤岗市| 青神县| 永春县| 鄂尔多斯市| 涟源市| 乾安县| 成都市|