Java并發(fā)編程——鎖機(jī)制

1、Java中的鎖(抽象角度)

鎖從樂觀和悲觀的角度可分為樂觀鎖悲觀鎖,從獲取資源的公平性角度可分為公平鎖非公平鎖,從是否共享資源的角度可分為共享鎖獨(dú)占鎖,從鎖的狀態(tài)的角度可分為偏向鎖、輕量級(jí)鎖重量級(jí)鎖。

  • 樂觀鎖悲觀鎖
  • 公平鎖非公平鎖
  • 共享鎖獨(dú)占鎖
  • 偏向鎖輕量級(jí)鎖重量級(jí)鎖
  • 可重入鎖
  • 自旋鎖

1.1 樂觀鎖和悲觀鎖

  1. 樂觀鎖

樂觀鎖采用樂觀的思想處理數(shù)據(jù),在每次讀取數(shù)據(jù)時(shí)都認(rèn)為別人不會(huì)修改該數(shù)據(jù),所以不會(huì)上鎖,但在更新時(shí)會(huì)判斷在此期間別人有沒有更新該數(shù)據(jù),通常采用在寫時(shí)先讀出當(dāng)前版本號(hào)然后加鎖的方法。具體過程為:比較當(dāng)前版本號(hào)與上一次的版本號(hào),如果版本號(hào)一致,則更新;如果版本號(hào)不一致,則重復(fù)進(jìn)行讀、比較、寫操作。

Java中樂觀鎖大部分是通過CAS(Compare And Swap,比較和交換)操作實(shí)現(xiàn)的,CAS是一種原子更新操作,在對(duì)數(shù)據(jù)操作之前首先會(huì)比較當(dāng)前值跟傳入的值是否一樣,如果一樣則更新,否則不執(zhí)行更行操作,直接返回失敗狀態(tài)。

  1. 悲觀鎖

悲觀鎖采用思想處理數(shù)據(jù),在每次讀取數(shù)據(jù)時(shí)都認(rèn)為別人會(huì)修改數(shù)據(jù),所以每次在讀寫數(shù)據(jù)時(shí)都會(huì)上鎖,這樣別人想讀寫這個(gè)數(shù)據(jù)時(shí)就會(huì)阻塞、等待直到拿到鎖。

Java中的悲觀鎖大部分基于AQS(Abstract Queued Synchronized,抽象的隊(duì)列同步器)架構(gòu)實(shí)現(xiàn)。AQS定義了一套多線程訪問共享資源的同步框架,許多同步類的實(shí)現(xiàn)都依賴于它,例如常用的Sychronized、ReentrantLock、Semaphore、CountDownLatch等。該框架下的鎖會(huì)先嘗試以CAS樂觀鎖去獲取鎖,如果獲取不到,則會(huì)轉(zhuǎn)為悲觀鎖(如ReentrantLock)。

1.2 公平鎖和非公平鎖

  1. 公平鎖

公平鎖指在分配鎖前檢查是否有線程在排隊(duì)等待獲取該鎖,優(yōu)先將鎖分配給排隊(duì)時(shí)間最長(zhǎng)的線程。

  1. 非公平鎖

非公平鎖指在分配鎖時(shí)不考慮線程排隊(duì)等待的情況,直接嘗試獲取鎖,在獲取不到時(shí)再排到隊(duì)尾等待。

因?yàn)楣芥i需要在多多核的情況下維護(hù)一個(gè)鎖線程等待隊(duì)列,基于該隊(duì)列進(jìn)行鎖的分配,因此效率比非公平鎖低很多。Java中的sychronized是非公平鎖,ReentrantLock默認(rèn)的lock方法采用的是非公平鎖。

1.3 共享鎖和獨(dú)占鎖

  1. 共享鎖

共享鎖允許多個(gè)線程同時(shí)獲取鎖資源,并發(fā)訪問共享資源。ReentranReadWriteLock中的讀鎖為共享鎖的實(shí)現(xiàn)。

  1. 獨(dú)占鎖

獨(dú)占鎖也叫互斥鎖,同一時(shí)刻只允許一個(gè)線程獲取鎖資源。ReentrantLock為獨(dú)占鎖的實(shí)現(xiàn)。

1.4 偏向鎖、輕量級(jí)鎖和重量級(jí)鎖

  1. 偏向鎖

除了在多線程之間存在競(jìng)爭(zhēng)獲取鎖的情況,還會(huì)經(jīng)常出現(xiàn)同一個(gè)鎖被同一個(gè)線程多次獲取的情況。偏向鎖用于在某個(gè)線程獲取某個(gè)鎖之后,消除這個(gè)線程鎖沖入的開銷,看起來似乎是這個(gè)線程得到了該鎖的偏向(偏袒)

偏向鎖的主要目的是在同一個(gè)線程多次獲取某個(gè)鎖的情況下盡量減少輕量級(jí)鎖的執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放需要多次CAS原子操作,而偏向鎖只需要在切換ThreadID時(shí)執(zhí)行一次CAS原子操作,因此可以提高鎖的運(yùn)行效率。

在出現(xiàn)多線程競(jìng)爭(zhēng)鎖的情況時(shí),JVM會(huì)自動(dòng)撤離偏向鎖,因此偏向鎖的撤銷操作的好事必須少于節(jié)省下來的 CAS原子操作的耗時(shí)。

  1. 輕量級(jí)鎖

輕量級(jí)鎖是相對(duì)于重量級(jí)鎖而言的。輕量級(jí)鎖的核心設(shè)計(jì)是在沒有多線程競(jìng)爭(zhēng)的前提下,減少重量級(jí)鎖的使用以提高系統(tǒng)性能。輕量級(jí)鎖適用于線程交替執(zhí)行同步代碼塊的情況(即互斥操作),如果同一時(shí)刻有多個(gè)線程訪問同一個(gè)鎖,則將會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。

  1. 重量級(jí)鎖

重量級(jí)鎖是基于操作系統(tǒng)的互斥量(Mutex Lock)而實(shí)現(xiàn)的鎖,會(huì)導(dǎo)致進(jìn)程在用戶態(tài)和內(nèi)核態(tài)之間切換,相對(duì)開銷較大。

synchronized在內(nèi)部基于監(jiān)視器鎖(Monitor)實(shí)現(xiàn),監(jiān)視器鎖基于底層的操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),因此synchronized屬于重量級(jí)鎖。

JDK在1.6版本后,為了減少獲取鎖和釋放鎖所帶來的性能消耗及提高性能,引入了輕量級(jí)鎖和偏向鎖。

鎖膨脹
鎖的狀態(tài)總共有4種:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖競(jìng)爭(zhēng)越來越激烈,鎖可能從偏向鎖升級(jí)到重量級(jí)鎖,但在Java中鎖只單項(xiàng)升級(jí),不會(huì)降級(jí)。

1.5 可重入鎖

可重入鎖也叫作遞歸鎖,指在同一線程中,在外層函數(shù)獲取到該鎖之后,內(nèi)層的遞歸函數(shù)仍然可以繼續(xù)獲取該鎖。簡(jiǎn)而言之,可重入鎖指該鎖能夠支持一個(gè)線程對(duì)同一個(gè)資源執(zhí)行多次加鎖操作。在Java環(huán)境下,ReentrantLock和sychronized都是可重入鎖。

1.6 自旋鎖

自旋鎖認(rèn)為:如果持有鎖的線程能在很短的時(shí)間內(nèi)釋放鎖資源,那么那些等待競(jìng)爭(zhēng)鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞、掛起狀態(tài),只需等一等(也叫作自旋),在等待持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免了用戶線程在內(nèi)核狀態(tài)的切換上導(dǎo)致的鎖時(shí)間消耗。

線程在自旋時(shí)會(huì)占用CPU,在線程長(zhǎng)時(shí)間自旋獲取不到鎖時(shí),將會(huì)產(chǎn)生CPU的浪費(fèi),甚至有時(shí)線程永遠(yuǎn)無法獲取鎖而導(dǎo)致CPU資源被永久占用,所以需要設(shè)定一個(gè)自旋等待的最大時(shí)間。在線程執(zhí)行的時(shí)間超過自旋等待的最大時(shí)間后,線程將會(huì)退出自旋模式并釋放其持有的鎖。

自旋鎖的優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn):自旋鎖可以減少CPU上下文的切換,對(duì)于占用鎖的時(shí)間非常短或鎖競(jìng)爭(zhēng)不激烈的代碼塊來說性能大幅度提升,因?yàn)樽孕腃PU耗時(shí)明顯少于線程阻塞、掛起、再喚醒時(shí)兩次CPU上下文切換所用的時(shí)間。
  • 缺點(diǎn):在持有鎖的線程占用鎖時(shí)間過長(zhǎng)或鎖的競(jìng)爭(zhēng)過于激烈時(shí),線程在自旋過程中會(huì)長(zhǎng)時(shí)間獲取不到鎖資源,將引起CPU的浪費(fèi)。所以在系統(tǒng)中有復(fù)雜鎖依賴的情況下不適合采用自旋鎖。

自旋鎖的時(shí)間閾值

如果自旋的執(zhí)行時(shí)間太長(zhǎng),則會(huì)有大量的線程處于自旋狀態(tài)且占用CPU資源,造成系統(tǒng)資源浪費(fèi)。因此,對(duì)自旋的周期選擇直接影響到系統(tǒng)的性能!
JDK的不同版本所采用的自旋周期不同,JDK1.5為固定時(shí)間,JDK1.6引入了適應(yīng)性自旋鎖。適應(yīng)性自旋鎖的自旋時(shí)間不再是固定值,而是由上一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定的,可基本認(rèn)為一個(gè)線程上下文切換的時(shí)間就是一個(gè)最佳時(shí)間。

2、Java中的鎖(實(shí)現(xiàn)角度)

2.1 synchronized

synchronized關(guān)鍵字用于為Java對(duì)象、方法、代碼塊提供線程安全的操作。synchronized屬于獨(dú)占式的悲觀鎖,同時(shí)屬于可重入鎖和非公平鎖。

Java中的每個(gè)對(duì)象都有個(gè)monitor對(duì)象,加鎖就是在競(jìng)爭(zhēng)monitor對(duì)象。對(duì)代碼塊加鎖是通過在前后分別加上monitorenter和monitorexit指令實(shí)現(xiàn)的,對(duì)方法是否加鎖是通過一個(gè)標(biāo)志位來判斷的。

synchronized的作用范圍

  • synchronized作用于成員變量和非靜態(tài)方法時(shí),鎖住的是對(duì)象的實(shí)例,即this對(duì)象。
  • synchronized作用于靜態(tài)方法時(shí),鎖住的是Class實(shí)例,因?yàn)殪o態(tài)方法屬于Class而不屬于對(duì)象。
  • synchronized作用于一個(gè)代碼塊時(shí),鎖住的是所有代碼塊中配置的對(duì)象。

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

在synchronized內(nèi)部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、!Owner這6個(gè)區(qū)域,每個(gè)區(qū)域的數(shù)據(jù)都代表鎖的不同狀態(tài)。

  • ContentionList:鎖競(jìng)爭(zhēng)隊(duì)列,所有請(qǐng)求鎖的線程都被放在競(jìng)爭(zhēng)隊(duì)列中。
  • EntryList:競(jìng)爭(zhēng)候選列表,在ContentionList中有資格成為候選者來競(jìng)爭(zhēng)鎖資源的線程被移動(dòng)到了EntryList中。
  • WaitSet:等待集合,調(diào)用wait方法后被阻塞的線程將被放在WaitSet中。
  • OnDeck:競(jìng)爭(zhēng)候選者,在同一時(shí)刻最多只有一個(gè)線程在競(jìng)爭(zhēng)鎖資源,該線程的狀態(tài)被成為OnDeck。
  • Owner:競(jìng)爭(zhēng)到鎖資源的線程被稱為Owner狀態(tài)線程。
  • !Owner:在Owner線程釋放鎖后,會(huì)從Owner的狀態(tài)變?yōu)?Owner。

synchronized在收到新的鎖請(qǐng)求時(shí)首先自旋,如果通過自旋也沒有獲取到鎖資源,則將被放入鎖競(jìng)爭(zhēng)隊(duì)列ContentionList中。該做法對(duì)于已經(jīng)進(jìn)入隊(duì)列的線程是不公平的,因此synchronized是非公平鎖。

2.2 volatile

volatile變量具備兩種特性:一種是保證該變量對(duì)所有線程可見,在一個(gè)線程修改了變量的值后,新的值對(duì)于其它線程是可以立即獲取的;一種是volatile禁止指令重排。
需要說明的是,volatile關(guān)鍵字可以嚴(yán)格保障變量的單次讀、寫操作的原子性,但并不能保證像i++這種操作的原子性,因?yàn)閕++在本質(zhì)上是讀、寫兩次操作。volatile在某些場(chǎng)景下可以代替synchronized,但是volatile不能完全取代synchronized的位置,只有在一些特殊場(chǎng)景下才適合使用volatile。比如,必須同時(shí)滿足下面兩個(gè)條件才能保證并發(fā)環(huán)境的線程安全:

  • 對(duì)變量的寫操作不依賴當(dāng)前(比如i++),或者說是單純的變量賦值(boolean flag = true)。
  • 該變量沒有被包含在具有其它變量的不變式中,也就是說在不同的volatile變量之間不能互相依賴,只有在狀態(tài)真正獨(dú)立于程序內(nèi)的其它內(nèi)容時(shí)才能使用volatile。

2.3 ReentrantLock

ReentrantLock繼承了Lock接口并實(shí)現(xiàn)了在接口中定義的方法,是一個(gè)可重入的獨(dú)占鎖,ReentrantLock支持公平鎖和非公平鎖的實(shí)現(xiàn)。ReentrantLock有顯式的操作過程,何時(shí)加鎖、何時(shí)釋放鎖都在程序員的控制之下。具體的使用流程是定義一個(gè)ReentrantLock,在需要加鎖的地方通過lock方法加鎖,等資源完成后再通過unlock方法釋放鎖。

Lock lock = new ReentrantLock();//ReentrantLock(true)參數(shù)為true表示公平鎖
try{
    lock.lock();//加鎖操作
}finally{
    lock.unlock();//釋放鎖操作
}

ReentrantLock不但提供了synchronized對(duì)鎖的操作功能,還提供了諸如響應(yīng)中斷鎖、可輪詢鎖請(qǐng)求、定時(shí)鎖等避免多線程死鎖的方法。

  • 響應(yīng)中斷:ReentrantLock對(duì)中斷是有響應(yīng)的。
  • 可輪詢鎖:通過boolean trytLock()獲取鎖。如果有可用鎖,則獲取該鎖并返回true,如果無可用鎖,則立即返回false。
  • 定時(shí)鎖:通過boolean trytLock(long time,TimeUnit unit) throws InterruptedException獲取定時(shí)鎖。如果在給定的時(shí)間內(nèi)獲取到了可用鎖,且當(dāng)前線程未被中斷,則獲取該鎖并返回true。如果在給定的時(shí)間內(nèi)獲取不到可用鎖,將禁用當(dāng)前線程。

2.4 CountDownLatch

CountDownLatch類位于java.util.concurrent包下,是一個(gè)同步工具類,允許一個(gè)或多個(gè)線程一直等待其他線程的操作執(zhí)行完后再執(zhí)行相關(guān)操作。
CountDownLatch基于線程計(jì)數(shù)器來實(shí)現(xiàn)并發(fā)訪問控制,主要用于主線程等待其它子線程都執(zhí)行完畢后執(zhí)行相關(guān)操作。其使用過程為:1)在主線程中定義CountDownLatch,并將線程計(jì)數(shù)器的值設(shè)置為子線程的個(gè)數(shù);2)多個(gè)子線程并發(fā)執(zhí)行,每個(gè)子線程在執(zhí)行完畢后都會(huì)調(diào)用countDown函數(shù)將計(jì)數(shù)器的值減1;3)直到線程計(jì)數(shù)器為0,表示所有的子線程任務(wù)都已執(zhí)行完畢,此時(shí)在CountDownLatch上等待的主線程將被喚醒并繼續(xù)執(zhí)行。

2.5 Semaphore

Semaphore是一種基于計(jì)數(shù)的信號(hào)量,在定義信號(hào)量對(duì)象時(shí)可以設(shè)定一個(gè)閾值,基于該閾值,多個(gè)線程競(jìng)爭(zhēng)獲取許可信號(hào),線程競(jìng)爭(zhēng)到許可信號(hào)后開始執(zhí)行具體的業(yè)務(wù)邏輯,業(yè)務(wù)邏輯在執(zhí)行完成后釋放該許可信號(hào)。在許可信號(hào)的競(jìng)爭(zhēng)隊(duì)列超過閾值后,新加入的申請(qǐng)?jiān)S可信號(hào)的線程將被阻塞,直到有其它許可信號(hào)被釋放。

Semaphore對(duì)鎖的申請(qǐng)和釋放跟ReentrantLock類似,通過acquire方法和release方法來獲取和釋放許可信號(hào)資源。Semaphore.acquire方法默認(rèn)和ReentrantLock.lockInterruptily方法的效果一樣,為可響應(yīng)中斷鎖,也就是說在等待許可信號(hào)資源的過程中可以被Thread.interrupt方法中斷而取消對(duì)許可信號(hào)的申請(qǐng)。

此外,Semaphore也實(shí)現(xiàn)了可輪詢的鎖請(qǐng)求、定時(shí)鎖的功能,以及公平鎖與非公平鎖的機(jī)制。對(duì)公平鎖和非公平鎖的定義在構(gòu)造函數(shù)中設(shè)定。

2.6 ReadWriteLock

為了提高性能,Java提供了讀寫鎖。讀寫鎖分為讀鎖和寫鎖兩種,多個(gè)讀鎖不互斥,讀寫與寫鎖互斥。在讀的地方使用讀鎖,在寫的地方使用寫鎖,在沒有寫鎖的情況下,讀是無阻塞的。

一般做法是分別定義一個(gè)讀鎖和一個(gè)寫鎖,在讀取共享數(shù)據(jù)時(shí)使用讀鎖,在使用完成后釋放讀鎖,在寫共享數(shù)據(jù)時(shí)使用寫鎖,在使用完成后釋放寫鎖。

2.7 CycleBarrier

CycleBarrier(循環(huán)屏障)是一個(gè)同步工具,可以實(shí)現(xiàn)讓一組線程等待至某個(gè)狀態(tài)之后再全部同時(shí)執(zhí)行。在所有等待線程被釋放之后,CycleBarrier可以被重用。CycleBarrier的等待狀態(tài)叫作Barrier狀態(tài),在調(diào)用await方法后,線程就處于Barrier狀態(tài)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容