生活中,我們會用鎖來保護自身的財產不被人偷走,但今天講的“鎖”,可不是這個用途。
在編程世界里,如何用好鎖是程序員的基本素養之一。多線程訪問共享資源的時候,避免不了資源競爭而導致數據錯亂的問題,通常為了解決這一問題,都會在訪問共享資源之前加鎖。最常用的就是互斥鎖,當然還有很多種不同的鎖,比如自旋鎖、讀寫鎖、樂觀鎖等,不同種類的鎖自然適用於不同的場景。
如果選擇了錯誤的鎖,在一些高並發的場景下,可能會降低系統的性能,影響用戶體驗。為了選擇合適的鎖,不僅需要清楚知道加鎖的成本開銷有多大,還需要分析業務場景中訪問的共享資源的方式,再來還要考慮並發訪問共享資源時的沖突概率。對症下葯,才能減少鎖對高並發性能的影響。
讀寫鎖(RWMutex)
讀寫鎖是針對於讀寫操作的互斥鎖。它與普通的互斥鎖最大的不同是,它可以分別針對讀操作和寫操作進行鎖定和解鎖操作。讀寫鎖遵循的訪問控制規則與互斥鎖有所不同。在讀寫鎖管轄的范圍內,它允許任意個讀操作的同時進行。但在同一時刻,它只允許有一個寫操作在進行。並且,在某一個寫操作被進行的過程中,讀操作的進行也是不被允許的。也就是說,讀寫鎖控制下的多個寫操作之間都是互斥的,並且寫操作與讀操作之間也都是互斥的。但多個讀操作之間卻不存在互斥關系。
- 讀鎖: 所有的 goroutine 都可以同時讀, 但不允許寫
- 寫鎖: 只允許一個goroutine 寫, 其他的goroutine 不允許讀也不允許寫
用golang 來演示讀寫鎖的作用,如下:

互斥鎖(Mutex)
使用互斥鎖(Mutex,全稱 mutual exclusion)是為了來保護一個資源不會因為並發操作而引起沖突,導致數據不准確。

互斥鎖與自旋鎖的區別
最底層的兩種就是「互斥鎖和自旋鎖」,有很多高級的鎖都是基於它們實現,可以認為它們是各種鎖的地基,所以得清楚它倆之間的區別和應用。
加鎖的目的是保證共享資源在任意時間里,只有一個線程訪問,這樣就可以避免多線程導致共享數據錯亂的問題。
當已經有一個線程加鎖后,其他線程加鎖則會失敗,互斥鎖和自旋鎖對於加鎖失敗后的處理方式是不一樣的。
互斥鎖與自旋鎖的開銷成本
互斥鎖加鎖失敗后,線程會釋放 CPU ,給其他線程;旋鎖加鎖失敗后,線程會忙等待,直到它拿到鎖;互斥鎖是一種「獨占鎖」,比如當線程 A 加鎖成功后,此時互斥鎖已經被線程 A 獨占了,只要線程 A 沒有釋放手中的鎖,線程 B 加鎖就會失敗,於是就會釋放 CPU 讓給其他線程,既然線程 B 釋放掉了 CPU,自然線程 B 加鎖的代碼就會被阻塞。
對於互斥鎖加鎖失敗而阻塞的現象,是由操作系統內核實現的。當加鎖失敗時,內核會將線程置為「睡眠」狀態,等到鎖被釋放后,內核會在合適的時機喚醒線程,當這個線程成功獲取到鎖后,就可以繼續執行。
樂觀鎖與悲觀鎖
前面提到的互斥鎖、自旋鎖、讀寫鎖,都是屬於悲觀鎖。悲觀鎖做事比較悲觀,它認為多線程同時修改共享資源的概率比較高,於是很容易出現沖突,所以訪問共享資源前,先要上鎖。
相反的,如果多線程同時修改共享資源的概率比較低,就可以采用樂觀鎖。樂觀鎖做事比較樂觀,它假定沖突的概率很低,它的工作方式是:先修改完共享資源,再驗證這段時間內有沒有發生沖突,如果沒有其他線程在修改資源,那么操作完成,如果發現有其他線程已經修改過這個資源,就放棄本次操作。
放棄后如何重試,這跟業務場景息息相關,雖然重試的成本很高,但是沖突的概率足夠低的話,還是可以接受的。可見,樂觀鎖的心態是,不管三七二十一,先改了資源再說。另外,你會發現樂觀鎖全程並沒有加鎖,所以它也叫無鎖編程。
舉個例子:
在線文檔是可以同時多人編輯,如果使用了悲觀鎖,只要有一個用戶正在編輯文檔,此時其他用戶就無法打開相同的文檔,用戶體驗當然不好。
實現多人同時編輯,實際上是用了樂觀鎖,它允許多個用戶打開同一個文檔進行編輯,編輯完提交之后才驗證修改的內容是否有沖突。
怎么樣才算發生沖突?這里舉個例子,比如用戶 A 先在瀏覽器編輯文檔,之后用戶 B 在瀏覽器也打開了相同的文檔進行編輯,但是用戶 B 比用戶 A 提交改動,這一過程用戶 A 是不知道的,當 A 提交修改完的內容時,那么 A 和 B 之間並行修改的地方就會發生沖突。
服務端是如何解決這種沖突的?
由於發生沖突的概率比較低,所以先讓用戶編輯文檔,但是瀏覽器在下載文檔時會記錄下服務端返回的文檔版本號;當用戶提交修改時,發給服務端的請求會帶上原始文檔版本號,服務器收到后將它與當前版本號進行比較,如果版本號一致則修改成功,否則提交失敗。
實際上,常見的 SVN 和 Git 也是用了樂觀鎖的思想,先讓用戶編輯代碼,然后提交的時候,通過版本號來判斷是否產生了沖突,發生了沖突的地方,需要我們自己修改后,再重新提交。
樂觀鎖雖然去除了加鎖解鎖的操作,但一旦發生沖突,重試的成本非常高,所以只有在沖突概率非常低,且加鎖成本非常高的場景時,才考慮使用樂觀鎖。