JAVA 多線程鎖介紹
1. 前言
本節內容主要是對 Java 多線程鎖進行介紹,是對鎖的一個全方位的概述,為我們對后續深入學習不同的鎖的使用方法奠定一個良好的基礎。本節內容的知識點如下:
- 樂觀鎖與悲觀鎖的概念,以及兩種鎖之間的區別,這是并發編程中經常涉及到的知識點,這是本節課程的核心知識點,是熱度很高的必須要掌握的知識,后續還會有專門的小節進行詳細講解;
- 公平鎖與非公平鎖的介紹,并發編程中經常涉及到的知識點,需要掌握其概念與區別;
- 獨占鎖與共享鎖的介紹,并發編程中經常涉及到的知識點,需要掌握其概念與區別;
- 自旋鎖的介紹,對于自旋鎖,了解其概念即可。
2. 悲觀鎖
定義:悲觀鎖指對數據被外界修改持保守態度,認為數據很容易就會被其他線程修改(很悲觀),所以在數據被處理前先對數據進行加鎖,并在整個數據處理過程中,使數據處于鎖定狀態。
悲觀鎖的實現:開發中常見的悲觀鎖實現往往依靠數據庫提供的鎖機制,即在數據庫中,在對數據記錄操作前給記錄加排它鎖。如果獲取鎖失敗,則說明數據正在被其他線程修改,當前線程則等待或者拋出異常。如果獲取鎖成功,則對記錄進行操作,然后提交事務后釋放排它鎖。
實例:Java 中的 synchronized 關鍵字就是一種悲觀鎖,一個線程在操作時,其他的線程必須等待,直到鎖被釋放才可進入方法進行執行,保證了線程和數據的安全性,同一時間,只能有一條線程進入執行。
我們用一段熟悉的代碼進行悲觀鎖的展示。
public class Student {
private String name;
public synchronized String getName() {
return name;
}
public synchronized void setName(String name) {
this.name = name;
}
}
代碼分析 :假設有 3 條線程,如下圖,線程 3 正在操作 Student 類,此時線程 1 和線程 2 必須要等待線程 3 執行完畢方可進入,這就是悲觀鎖。
3. 樂觀鎖
定義:樂觀鎖是相對悲觀鎖來說的,它認為數據在一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在進行數據提交更新的時候,才會正式對數據沖突與否進行檢測。
樂觀鎖的實現:依舊拿數據庫的鎖進行比較介紹,樂觀鎖并不會使用數據庫提供的鎖機制, 一般在表中添加 version 宇段或者使用業務狀態來實現。 樂觀鎖直到提交時才鎖定,所以不會產生任何死鎖。
Java 中的樂觀鎖:我們之前所學習的 CAS 原理即是樂觀鎖技術,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試。
Tips:我們這里所說的對于樂觀鎖,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗。注意失敗兩字,失敗意味著有操作,而悲觀鎖是等待,意味著不能同時操作。
4. 悲觀鎖機制存在的問題
- 在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題;
- 一個線程持有鎖會導致其它所有需要此鎖的線程掛起;
- 如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。
對比于悲觀鎖的這些問題,另一個更加有效的鎖就是樂觀鎖。其實樂觀鎖就是:每次不加鎖而是假設沒有并發沖突而去完成某項操作,如果因為并發沖突失敗就重試,直到成功為止。
5. 公平鎖與非公平鎖
分類:根據線程獲取鎖的搶占機制,鎖可以分為公平鎖和非公平鎖。
公平鎖:表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。
非公平鎖:非公平鎖則在運行時闖入,不遵循先到先執行的規則。
ReentrantLock:ReentrantLock 提供了公平和非公平鎖的實現。我們本節只做介紹,后續章節會對 ReentrantLock 進行深入的講解。
6. 獨占鎖與共享鎖
分類:根據鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨占鎖和共享鎖。
獨占鎖:保證任何時候都只有一個線程能得到鎖,ReentrantLock 就是以獨占鎖方式實現的。
共享鎖:則可以同時由多個線程持有,例如 ReadWriteLock 讀寫鎖,它允許一個資源可以被多線程同時進行讀操作。
獨占鎖是一種悲觀鎖,由于每次訪問資源都先加上互斥鎖,這限制了并發性,因為讀操作并不會影響數據的一致性,而獨占鎖只允許在同一時間由一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取。
共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。
7. 自旋鎖
由于 Java 中的線程是與操作系統中的線程相互對應的,所以當一個線程在獲取鎖(比如獨占鎖)失敗后,會被切換到內核狀態而被掛起。
當該線程獲取到鎖時又需要將其切換到內核狀態而喚醒該線程。而從用戶狀態切換到內核狀態的開銷是比較大的,在一定程度上會影響并發性能。
自旋鎖:自旋鎖則是當前線程在獲取鎖時,如果發現鎖已經被其他線程占有,它不馬上阻塞自己,在不放棄 CPU 使用權的情況下,多次嘗試獲?。J次數是 10,可以使用-XX:PreBlockSpinsh 參數設置該值)。
很有可能在后面幾次嘗試中其他線程己經釋放了鎖。如果嘗試指定的次數后仍沒有獲取到鎖則當前線程才會被阻塞掛起。由此看來自旋鎖是使用 CPU 時間換取線程阻塞與調度的開銷,但是很有可能這些 CPU 時間白白浪費了。
8. 小結
本節內容為鎖的基礎概念介紹,其中對于悲觀鎖和樂觀鎖的講解,為本節核心知識點,因為這兩種鎖在數據庫的領域也是應用十分廣泛。對于本節提到的具體的鎖實現,如 ReentrantLock ,我們在后續章節中會有詳細的講解。
本節內容為基礎介紹,掌握本節內容的基礎上,能夠更好地學習后續的章節知識。