【談一談】并發編程_鎖的分類
Hello!~
大家好!~每天進步一點點,日復一日,我們終將問劍頂峰這里主要是介紹下我們常用的鎖可以分為幾類,目的是整體框架作用~方便后續的并發文章
說白了,這篇就是開頭哈~
本文總綱:
一.可重入鎖和不可重入鎖
我們開發中一般用到的都是可重入鎖比如
Synchronized
,ReentrantReadWriteLock
,ReentrantLock
都是可重入的不可重入的鎖很少見,也不怎么用到(如果要用,一般都自己通過
Lock
定義實現)
1.可重入鎖:
它也稱為遞歸鎖,啥意思呢?
- 是指同一線程在獲取鎖(如A鎖)之后,可以再次對該鎖(A)進行獲取,而不會造成死鎖。
- 這種鎖支持同一個線程對資源的重復加鎖,并且在釋放鎖時,必須是獲取鎖的次數與釋放鎖的次數相等時,才會真正釋放鎖
還是有點迷糊嗎?我們舉個Java
中ReentrantLock
實現可重入鎖的簡單例子來加深理解:
ReentrantLock
的鎖機制:(我先說下,不然看下面代碼,估計不怎么好理解哈)
- 內部維護了一個計數器,
- 每當線程獲取鎖時,計數器加1;每當線程釋放鎖時,計數器減1;
- 只有當計數器為0時,其他線程才有機會獲取該鎖
下面代碼:
doSomething()
方法調用了doSomethingElse()
方法,由于ReentrantLock
的可重入特性,第二次調用lock()
不會導致線程阻塞,而是使鎖計數器加1。- 當對應的
unlock()
方法被調用兩次后,鎖才會真正釋放,允許其他線程獲取鎖
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 獲取鎖try {// 在這里執行臨界區代碼doSomethingElse();} finally {lock.unlock(); // 無論是否發生異常,最終都要釋放鎖}}private void doSomethingElse() {lock.lock(); // 同一線程內再次獲取鎖,不會阻塞try {// 在這里執行另一段臨界區代碼} finally {lock.unlock(); // 釋放鎖}}
}
2.不可重入鎖
是指一個線程獲取鎖后,在未釋放該鎖的情況下**,無法再次獲取**該鎖的同步機制,必須等此鎖釋放后,才能再獲取鎖
細心的同學可能會發現,在
Java
的標準庫中,并沒有直接提供不可重入鎖的實現,為什么??
- 因為在多層級調用或遞歸場景下(大多數的并發場景中),它們很容易造成意外死鎖問題,
- 而可重入鎖(如
ReentrantLock
)則可以安全地支持這些復雜情況。
舉個例子:(不考慮復雜場景哈~你別杠,我們只是演示下,目的是懂)
通過簡單的計數器模擬,當鎖被獲取時,增加計數器,有且僅有計數器為0是才允許獲取鎖
下面的代碼:
- 這樣的鎖如果在線程內部遞歸調用
lock
方法,將會導致后續嘗試獲取鎖的操作阻塞,從而表現出不可重入的特性。
public class NonReentrantLock {private boolean isLocked = false;private Thread holdingThread;public synchronized void lock() throws InterruptedException {while (isLocked && holdingThread != Thread.currentThread()) {wait();}isLocked = true;holdingThread = Thread.currentThread();}public synchronized void unlock() {if (holdingThread == Thread.currentThread()) {isLocked = false;holdingThread = null;notifyAll();} else {throw new IllegalMonitorStateException("當前線程并未持有此鎖");}}
}
二.樂觀鎖和悲觀鎖
1.樂觀鎖 (Optimistic Locking
)
- 是一種在讀取數據時不會立即加鎖,而是在更新數據時才會檢查在此期間是否有其他事務對數據進行了修改的并發控制策略。
- 它假設大多數情況下不會有沖突發生(很樂觀吧~哈哈哈,),因此在進行數據操作時保持樂觀態度。
在補充下:
在數據庫系統中,樂觀鎖通常通過版本號或時間戳等機制實現。
- 當一個事務準備更新數據時,它會首先檢查該數據的版本號或時間戳是否與最初讀取時一致,
- 如果一致: 則執行更新操作并更新版本號或時間戳;
- 如果不一致,則表示在此期間有其他事務對該數據進行了修改,此時當前事務通常會選擇回滾以避免覆蓋其他事務的更改。
例子:
如
Hibernate
中使用@Version
注解:
- 每次更新
MyEntity
實例時,Hibernate都會自動檢查并更新version字段,從而實現了樂觀鎖的效果又如
Java
中提供的CAS
操作,典型的樂觀鎖實現
再舉個實際點: 一個整數版本號來模擬樂觀鎖機制
transfer
方法嘗試進行轉賬操作時,
- 首先記錄下當前賬戶的版本號。
- 然后模擬可能存在其他事務的情況,這里簡單地直接增加版本號以示意圖。
- 最后,在真正執行更新操作前,再次檢查版本號是否與最初讀取時一致。
- 如果一致,則執行轉賬邏輯并遞增版本號;
- 如果不一致,則表示存在并發沖突,轉賬操作失敗。
這樣就實現了一個基于版本號的樂觀鎖機制,它可以防止在并發環境下的數據不一致性問題。
public class Account {private int balance; // 賬戶余額private int version; // 數據版本號public Account(int initialBalance) {this.balance = initialBalance;this.version = 0;}// 使用樂觀鎖進行轉賬操作public boolean transfer(Account to, int amount) {// 保存當前賬戶和目標賬戶的原始版本號int originalVersion = this.version;// 模擬并發環境下可能出現的其他事務操作simulateOtherTransactions();// 嘗試更新賬戶余額和版本號if (this.version == originalVersion) {// 更新前檢查版本號未變if (this.balance >= amount) {this.balance -= amount;to.balance += amount;// 成功更新數據后,將版本號遞增this.version++;return true;} else {System.out.println("余額不足,轉賬失敗");}} else {System.out.println("并發沖突,有其他事務修改了賬戶數據,轉賬失敗");}return false;}// 模擬在轉賬操作過程中可能存在的其他事務對賬戶數據的修改private void simulateOtherTransactions() {// 這里僅用于演示,在實際應用中可能是由其他線程或事務引起的// 假設另一個事務在此時修改了賬戶數據并增加了版本號this.version++;}
}
2.悲觀鎖(Pessimistic Locking
)
獲取不到鎖資源時,會將當前線程掛起(進入
Blocked
或者waitting
)(有著一種生于憂患意識)官方術語:
- 是一種在訪問數據時假設會發生并發沖突,并立即對數據進行加鎖以防止其他事務或線程對其進行修改的并發控制策略。
- 傾向于認為每次對數據的操作都可能引發并發問題,所以在獲取數據前就先鎖定資源
這種操作例子在數據庫層面經常能看見:
如: 當第一個事務執行
SELECT ... FOR UPDATE
時,
- 會對當前的查詢記錄進行鎖定,此時其他任何事務試圖讀取或修改這條記錄都會被阻塞,直到第一個事務提交或回滾并釋放鎖
如: 在
Java
應用層面,
JDBC
中的java.sql.Connection
提供的setAutoCommit(false)
方法:- 可以開啟手動事務管理,配合數據庫的悲觀鎖機制實現更細粒度的并發控制。
再舉個: 以synchronized
關鍵字為例,提供一個簡單的線程安全的銀行賬戶轉賬操作:
transfer()
方法通過synchronized
關鍵字修飾,這意味著在同一時間只能有一個線程訪問這個方法。當一個線程調用
transfer
進行轉賬操作時,其他線程必須等待當前線程完成操作并釋放鎖后才能繼續執行。這就是悲觀鎖的應用,它假設并發環境下會存在數據沖突,并直接對資源進行鎖定,以防止多個線程同時修改共享資源導致的數據不一致問題。
public class BankAccount {private double balance;public BankAccount(double initialBalance) {this.balance = initialBalance;}// 使用synchronized實現悲觀鎖public synchronized void transfer(BankAccount to, double amount) throws InterruptedException {if (this.balance >= amount) {// 鎖定當前對象(即鎖定該方法),確保同一時間只有一個線程能執行此方法Thread.sleep(100); // 模擬耗時操作,如數據庫查詢或更新this.balance -= amount;to.balance += amount;System.out.println("From: " + Thread.currentThread().getName() + ", Transfer " + amount + " completed.");} else {throw new IllegalArgumentException("Insufficient balance.");}}public static void main(String[] args) {BankAccount accountA = new BankAccount(100);BankAccount accountB = new BankAccount(0);Thread thread1 = new Thread(() -> accountA.transfer(accountB, 50));Thread thread2 = new Thread(() -> accountA.transfer(accountB, 60));thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final balances: A=" + accountA.balance + ", B=" + accountB.balance);}
}
三.公平鎖和非公平鎖
這當中的關鍵點就是: 是否正常排隊
1.公平鎖
- 是一種線程調度策略,它保證了等待鎖的線程按照它們請求鎖的順序獲得鎖。
- 在公平鎖機制下,當鎖釋放時,會優先分配給已經在隊列中等待時間最長的線程,而不是隨機選擇一個等待的線程。
注意:
- 在公平鎖環境下,如果有多個線程在等待獲取鎖,那么鎖會被分配給等待時間最久的那個線程,這種策略能夠減少
"線程饑餓"
(即某些線程長時間無法獲取到鎖)的問題,提高系統的整體公平性- 公平鎖雖然在理論上提供了更好的公平性,但可能會降低系統的整體吞吐量,
- 因為每次釋放鎖時都需要維護和檢查等待隊列,并且需要考慮線程上下文切換的成本。
- 而在非公平鎖(默認情況下)中,獲取鎖的線程可能是最近剛剛嘗試獲取鎖的線程,這可能導致更高的并發性和系統性能,但可能也會導致某些線程長期得不到執行機會。
舉個例子:
假設我們有一個共享資源(一個計數器)需要多個線程安全地進行遞增操作:
ReentrantLock
類:參數設為true
,可以創建一個公平鎖在這個例子中:
我們創建了一個公平鎖,并在一個共享的計數器上進行了遞增操作。
當多個線程同時調用
increment()
方法時,公平鎖會確保等待時間最長的線程優先獲得鎖并執行操作。由于每個線程在操作后都休眠了100毫秒,這有助于模擬實際的并發環境,使得不同線程之間的執行順序更易于觀察。
在公平鎖策略下,理論上線程獲取鎖的順序將盡可能按照它們請求鎖的時間順序進行,因此輸出的結果應能體現出相對有序的執行過程。
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;public class FairLockExample {private final ReentrantLock lock = new ReentrantLock(true); // 創建一個公平鎖private int counter = 0;public void increment() {lock.lock();try {counter++;System.out.println(Thread.currentThread().getName() + " incremented the counter to: " + counter);Thread.sleep(100); // 模擬耗時操作} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {FairLockExample example = new FairLockExample();IntStream.range(0, 10).forEach(i -> new Thread(() -> example.increment(), "Thread-" + i).start());Thread.sleep(5000); // 等待所有線程執行完成System.out.println("Final counter value: " + example.counter);}
}
2.非公平鎖
是一種線程調度策略,與公平鎖相反,
- 在釋放鎖后并不保證等待時間最長的線程一定能獲得鎖。
- 當鎖可用時,非公平鎖可能會允許任何一個正在等待獲取鎖的線程獲取鎖,即使有其他線程已經等待了更長的時間。(適者生存,能者居之)
模擬場景說明:
線程
A
獲取到鎖資源,線程B
沒有拿到,線程B
去排隊,這時線程C
跑來了,線程C
咋么做呢?
- 首先去嘗試競爭一波
- 競爭成功: 拿到鎖,美滋滋進行執行
- 競爭失敗: 沒有拿到鎖資源,老老實實的排到
B
的后面,直到B
拿到鎖資源或者B
取消后,才去競爭鎖資源
舉個例子:
使用非公平鎖實現多線程安全遞增操作的例子
在這個例子中,
- 我們創建了一個非公平鎖,并在一個共享的計數器上進行了遞增操作。
- 當多個線程同時調用
increment()
方法時,非公平鎖可能讓任何等待鎖的線程獲取到鎖,而不考慮它們等待的先后順序。- 因此,輸出的結果可能顯示出線程獲取鎖和執行的相對無序性。
- 雖然非公平鎖可能導致某些線程
“饑餓”
(長時間無法獲取鎖),但在某些情況下,它能提供更高的吞吐量,因為減少了線程上下文切換的成本和隊列維護的開銷。
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;public class NonFairLockExample {private final ReentrantLock lock = new ReentrantLock(); // 創建一個非公平鎖(默認false)private int counter = 0;public void increment() {lock.lock();try {counter++;System.out.println(Thread.currentThread().getName() + " incremented the counter to: " + counter);Thread.sleep(100); // 模擬耗時操作} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {NonFairLockExample example = new NonFairLockExample();IntStream.range(0, 10).forEach(i -> new Thread(() -> example.increment(), "Thread-" + i).start());Thread.sleep(5000); // 等待所有線程執行完成System.out.println("Final counter value: " + example.counter);}
}