天天看點

互斥鎖、自旋鎖、讀寫鎖、條件鎖、悲觀鎖、樂觀鎖

互斥鎖和自旋鎖:

互斥鎖用保證共享資源在任一時間隻有一個線程通路,當已經有一個線程加鎖後,其他線程加鎖就會失敗,互斥鎖和自旋鎖對于加鎖失敗後的處理方式是不一樣的:互斥鎖加鎖失敗後,線程會釋放CPU,給其他線程;自旋鎖加鎖失敗後,線程會忙等待,直到它拿到鎖。

互斥鎖是一種獨占鎖,當線程A加鎖成功時,互斥鎖就被線程A獨占了,隻要線程A沒有釋放手中的鎖,線程B加鎖就會失敗,線程B被阻塞,釋放CPU。

互斥鎖加鎖失敗線程阻塞是由作業系統核心實作的。當加鎖失敗時,核心會将線程置位睡眠狀态,等到鎖被釋放後,核心會在合适的時機喚醒線程,當這個線程擷取到鎖後就可以繼續執行下去。

是以互斥鎖加鎖失敗時,會從使用者态陷入到核心态,讓核心幫我們切換線程,存在兩次線程上下文切換的成本:

1.線程加鎖失敗時,核心把線程從運作設定為睡眠,然後把CPU切換給其他程序。

2.當鎖被釋放時,之前睡眠的線程會變為就緒态,然後核心會在合适的時間把CPU切換給該線程運作。

線程的上下文切換:當兩個線程是屬于同一個程序,因為線程共享程序的虛拟記憶體,隻需要切換線程的私有資料,寄存器等不共享的資料。

如果被鎖住的代碼執行時間很短,小于兩次上下文切換的時間,就不應該使用互斥鎖,而應該選用自旋鎖。

自旋鎖通過CPU提供的CAS函數(Compare And Swap),在使用者态完成加鎖和解鎖操作,不會産生線程的上下文切換,是以速度開銷小一些。CAS函數是一條原子指令,包含兩個步驟:1.檢視所得狀态 2.如果鎖是空閑的,就将鎖設定為目前線程持有。

自旋鎖是比較簡單的一種鎖,一直自旋,利用CPU,直到鎖可用。需要注意,在單核CPU上,需要搶占式排程(不斷通過時鐘終端一個線程,運作其他線程)。如果采用非搶占式排程,會導緻一個自旋的線程永遠不會放棄CPU。

如果被鎖住的代碼執行時間過長,自旋的線程會長時間占用CPU資源,是以自旋的時間和被鎖住代碼的執行時間是成正比的關系。

自旋鎖和互斥鎖使用層面比較相似,但實作層面上完全不同:當加鎖失敗時,互斥鎖用線程切換來應對,自旋鎖則用忙等待來應對。

互斥鎖和自旋鎖時最底層的兩種鎖,更進階的鎖都會選擇其中一個來實作。比如讀寫鎖既可以選擇互斥鎖實作,也可以基于自旋鎖實作。

讀寫鎖:​

從字面意思也可以知道,它由讀鎖和寫鎖兩部分構成,如果隻讀取共享資源用讀鎖加鎖,如果要修改共享資源則用寫鎖加鎖。

讀寫鎖适用于能明确區分讀操作和寫操作的場景。

工作原理:

當寫鎖沒有被線程持有時,多個線程能夠并發的持有讀鎖,大大提高共享資源的通路效率。但是,一旦寫鎖被程序持有後,其他線程擷取讀鎖和寫鎖都會被阻塞。

是以說,寫鎖是獨占鎖,因為任何時刻隻能有一個線程持有寫鎖,類似互斥鎖和自旋鎖,而讀鎖是共享鎖,因為讀鎖可以被多個線程同時持有。知道了讀寫鎖的工作原理後,我們可以發現,讀寫鎖在讀多寫少的場景,能發揮出優勢。

另外,根據實作的不同,讀寫鎖可以分為讀優先鎖和寫優先鎖。

讀優先鎖:當讀線程A先持有了讀鎖,線程B在擷取寫鎖的時候,會被阻塞,并且在堵塞的過程中,後續來的讀線程C仍然可以成功擷取讀鎖,最後知道讀線程A和C釋放讀鎖後,寫線程B才可以成功擷取寫鎖。

寫優先鎖:當都線程A先持有了讀鎖,線程B在擷取寫鎖的時候,會被阻塞,并且在阻塞過程中,後續來的讀線程C在擷取讀鎖時也會被阻塞,這樣隻要讀線程A釋放讀鎖後,寫線程B就可以擷取寫鎖。

讀優先鎖對于讀線程的并發性更好,但是會導緻寫線程饑餓現象。寫優先鎖可以保證寫線程不會餓死,但是如果一直有寫線程擷取寫鎖,讀線程也會被餓死。

不管是讀優先鎖還是寫優先鎖,對方都可能會出現餓死的問題,是以不偏袒任何一方,搞個公平讀寫鎖:

用隊列把擷取鎖的線程排隊,不管是讀線程還是寫線程都按照先進先出的原則加鎖即可,這樣讀線程仍然可以并發,也不會出現饑餓的現象。

樂觀鎖與悲觀鎖:​

互斥鎖,自旋鎖和讀寫鎖都屬于悲觀鎖。悲觀鎖做事比較悲觀,它認為多線程同時修改共享資源的機率比較高,很容易出現沖突,是以通路共享資源前,要先上鎖。

相反的,如果多線程同時修改共享資源的機率比較低, 就可以采用樂觀鎖。樂觀鎖做事比較樂觀,它鑒定沖突的機率很低,它的工作方式是:先修改完共享資源,再驗證這段時間内有沒有發生沖突,如果沒有其他線程在修改資源,那麼操作完成,如果發現有其他線程已經修改過這個資源,就放棄本次操作。放棄後重試的成本很高,但是如果沖突的機率足夠低的話,還是可以接受的。可見,樂觀鎖的心态是,不管三七二十一,先改了資源再說。另外,樂觀鎖全程并沒有加鎖,是以它也叫無鎖程式設計。

樂觀鎖的應用場景:線上文檔。線上文檔是可以同時多人編輯的,如果使用了悲觀鎖,那麼隻要有一個使用者在編輯文檔,其他使用者就無法打開相同的文檔了。樂觀鎖允許多個使用者打開同一個文檔進行編輯,編輯完送出後才驗證修改的内容是否有沖突。沖突的例子:使用者A和B打開相同的文檔進行編輯,但是使用者B比使用者A先送出改動,這一過程使用者A是不知道的,當A送出修改完的内容時,那麼A和B并行修改的地方就會發生沖突。伺服器驗證發生沖突的方案:使用者的浏覽器在下載下傳文檔時會記錄服務端傳回的文檔版本号,當使用者送出修改時,發給伺服器的請求會帶上該文檔版本号,伺服器将受到的版本号與目前版本号進行比較,如果一緻則修改成功,否則修改失敗。

實際上,我們常見的SVN和Git也使用了樂觀鎖的思想,先讓使用者編輯代碼,然後送出的時候,通過版本号來判斷是否産生了沖突,發生了沖突的低檔,需要我們自己修改後,再重新送出。樂觀鎖雖然去除了加鎖解鎖的操作,但是一旦發生沖突,重試的成本非常高,是以隻有在沖突(多線程同時修改共享資源)機率非常低,且加鎖成本非常高的場景時,才考慮使用樂觀鎖。

繼續閱讀