1.悲觀鎖
悲觀鎖比較悲觀,它認為如果不鎖住這個資源,別的線程就會來爭搶,就會造成數據結果錯誤,所以悲觀鎖為了確保結果的正確性,會在每次獲取并修改數據時,都把數據鎖住,讓其他線程無法訪問該數據,這樣就可以確保數據內容萬無一失。
舉個例子
- 假設線程 A 和 B 使用的都是悲觀鎖,所以它們在嘗試獲取同步資源時,必須要先拿到鎖。
- 假設線程 A 拿到了鎖,并且正在操作同步資源,那么此時線程 B 就必須進行等待。
- 而當線程 A 執行完畢后,CPU 才會喚醒正在等待這把鎖的線程 B 再次嘗試獲取鎖。
- 如果線程 B 現在獲取到了鎖,才可以對同步資源進行自己的操作。這就是悲觀鎖的操作流程。
2.樂觀鎖
樂觀鎖比較樂觀,認為自己在操作資源的時候不會有其他線程來干擾,所以并不會鎖住被操作對象,不會不讓別的線程來接觸它,同時,為了確保數據正確性,在更新之前,會去對比在我修改數據期間,數據有沒有被其他線程修改過:如果沒被修改過,就說明真的只有我自己在操作,那我就可以正常的修改數據;
如果發現數據和我一開始拿到的不一樣了,說明其他線程在這段時間內修改過數據,那說明我遲了一步,所以我會放棄這次修改,并選擇報錯、重試等策略。
樂觀鎖的實現一般都是利用 CAS 算法實現的。
舉個例子:
- 假設線程 A 此時運用的是樂觀鎖。那么它去操作同步資源的時候,不需要提前獲取到鎖,而是可以直接去讀取同步資源,并且在自己的線程內進行計算。
- 當它計算完畢之后、準備更新同步資源之前,會先判斷這個資源是否已經被其他線程所修改過。
- 如果這個時候同步資源沒有被其他線程修改更新,也就是說此時的數據和線程 A 最開始拿到的數據是一致的話,那么此時線程 A 就會去更新同步資源,完成修改的過程。
- 而假設此時的同步資源已經被其他線程修改更新了,線程 A 會發現此時的數據已經和最開始拿到的數據不一致了,那么線程 A 不會繼續修改該數據,而是會根據不同的業務邏輯去選擇報錯或者重試。
3.相關用法
悲觀鎖:synchronized 關鍵字和 Lock 接口
Java 中悲觀鎖的實現包括 synchronized 關鍵字和 Lock 相關類等,我們以 Lock 接口為例,例如 Lock 的實現類 ReentrantLock,類中的 lock() 等方法就是執行加鎖,而 unlock() 方法是執行解鎖。處理資源之前必須要先加鎖并拿到鎖,等到處理完了之后再解開鎖,這就是非常典型的悲觀鎖思想。
樂觀鎖:原子類
樂觀鎖的典型案例就是原子類,例如 AtomicInteger 在更新數據時,就使用了樂觀鎖的思想,多個線程可以同時操作同一個原子變量。
數據庫
- 數據庫中同時擁有悲觀鎖和樂觀鎖的思想。例如,我們如果在 MySQL 選擇 select for update 語句,那就是悲觀鎖,在提交之前不允許第三方來修改該數據,這當然會造成一定的性能損耗,在高并發的情況下是不可取的。
- 相反,我們可以利用一個版本 version 字段在數據庫中實現樂觀鎖。在獲取及修改數據時都不需要加鎖,但是我們在獲取完數據并計算完畢,準備更新數據時,會檢查版本號和獲取數據時的版本號是否一致,如果一致就直接更新,如果不一致,說明計算期間已經有其他線程修改過這個數據了,那我就可以選擇重新獲取數據,重新計算,然后再次嘗試更新數據。
UPDATE studentSET name = ‘小李’,version= 2WHERE id= 100AND version= 1
4.使用場景
悲觀鎖適合用于并發寫入多、臨界區代碼復雜、競爭激烈等場景,這種場景下悲觀鎖可以避免大量的無用的反復嘗試等消耗。
樂觀鎖適用于大部分是讀取,少部分是修改的場景,也適合雖然讀寫都很多,但是并發并不激烈的場景。在這些場景下,樂觀鎖不加鎖的特點能讓性能大幅提高。