Java的樂觀鎖,悲觀鎖,讀寫鎖,遞歸鎖
我們都知道在 Java 中為了保證一些操作的安全性,就會(huì)涉及到使用鎖,但是你對(duì) Java 的鎖了解的有多少呢?Java 都有哪些鎖?以及他們是怎么實(shí)現(xiàn)的,今天了不起就來說說關(guān)于 Java 的鎖。
樂觀鎖
樂觀鎖(Optimistic Locking)是一種在數(shù)據(jù)讀取時(shí)不會(huì)阻塞其他讀取或?qū)懭氩僮鞯逆i策略,但在更新時(shí)會(huì)檢查在此期間是否有其他操作修改了數(shù)據(jù)。如果數(shù)據(jù)已被修改,則更新操作會(huì)失敗,通常是通過重試或拋出異常來處理。
在 Java 中,樂觀鎖通常是通過版本號(hào)、時(shí)間戳或其他狀態(tài)信息來實(shí)現(xiàn)的。以下是樂觀鎖在 Java 中的一些常見實(shí)現(xiàn)方式:
版本號(hào)機(jī)制:
- 數(shù)據(jù)表中增加一個(gè)“版本號(hào)”字段。
- 讀取數(shù)據(jù)時(shí),同時(shí)讀取版本號(hào)。
- 更新數(shù)據(jù)時(shí),將版本號(hào)加1,并帶上WHERE子句,確保版本號(hào)與讀取時(shí)的一致。
- 如果更新影響的行數(shù)為0,則表示在此期間數(shù)據(jù)已被其他事務(wù)修改。
時(shí)間戳機(jī)制:
- 類似于版本號(hào),但使用時(shí)間戳字段代替。
- 更新時(shí)檢查時(shí)間戳字段,確保它與讀取時(shí)的時(shí)間戳匹配。
CAS (Compare-and-Swap) 操作:
- 是一種原子操作,用于在多線程環(huán)境中安全地更新共享變量。
- CAS操作包括三個(gè)參數(shù):內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。
- 如果內(nèi)存位置V的值與預(yù)期原值A(chǔ)匹配,則將V的值更新為新值B。否則,不執(zhí)行任何操作。
- Java 的 AtomicInteger、AtomicLong 等原子類就使用了CAS操作。
JPA 和 Hibernate 的樂觀鎖:
- JPA 和 Hibernate 提供了內(nèi)置的樂觀鎖支持。
- 在實(shí)體類中添加一個(gè)版本號(hào)或時(shí)間戳字段,并使用 @Version 注解標(biāo)記。
- 當(dāng) Hibernate 或 JPA 嘗試更新一個(gè)實(shí)體時(shí),它會(huì)自動(dòng)檢查版本號(hào)或時(shí)間戳字段,以確保數(shù)據(jù)在此期間沒有被其他事務(wù)修改。
悲觀鎖
悲觀鎖(Pessimistic Locking)是一種在數(shù)據(jù)處理過程中,總是假設(shè)最壞的情況來避免數(shù)據(jù)并發(fā)問題的鎖策略。在Java中,悲觀鎖通常在數(shù)據(jù)被訪問時(shí)就立即加鎖,以保證在此期間其他任何事務(wù)都不能修改這個(gè)數(shù)據(jù),直到該事務(wù)完成為止。
Java中實(shí)現(xiàn)悲觀鎖的常見方式有以下幾種:
數(shù)據(jù)庫(kù)行級(jí)鎖和表級(jí)鎖:
- 行級(jí)鎖:對(duì)正在訪問的數(shù)據(jù)行加鎖,防止其他事務(wù)修改該行。這是數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)提供的一種鎖機(jī)制,可以通過SQL語(yǔ)句來實(shí)現(xiàn)。
- 表級(jí)鎖:對(duì)整個(gè)表加鎖,限制其他事務(wù)對(duì)該表的并發(fā)訪問。這種鎖的開銷較小,但并發(fā)性能較低。
Java中的synchronized關(guān)鍵字:
- synchronized是Java語(yǔ)言內(nèi)建的線程同步機(jī)制,它可以用來修飾方法或者以代碼塊的形式出現(xiàn)。當(dāng)一個(gè)線程進(jìn)入一個(gè)synchronized修飾的方法或代碼塊時(shí),它會(huì)獲取一個(gè)鎖,其他嘗試進(jìn)入該區(qū)域的線程將會(huì)被阻塞,直到第一個(gè)線程釋放鎖。
ReentrantLock類:
- Java的java.util.concurrent.locks.ReentrantLock類提供了重入鎖的實(shí)現(xiàn),這是一種悲觀鎖。與synchronized相比,ReentrantLock提供了更高的靈活性,比如可以嘗試獲取鎖、定時(shí)獲取鎖以及中斷等待鎖的線程等。
讀寫鎖(ReadWriteLock):
- java.util.concurrent.locks.ReadWriteLock接口定義了讀取和寫入鎖的規(guī)則。雖然它本身不是悲觀鎖,但其中的寫鎖部分是一種悲觀鎖策略。寫鎖會(huì)阻止其他線程進(jìn)行讀和寫操作,直到持有鎖的線程釋放它。
分布式鎖:
- 在分布式系統(tǒng)中,悲觀鎖的概念可以擴(kuò)展到跨多個(gè)進(jìn)程或機(jī)器。常見的實(shí)現(xiàn)方式包括使用Redis、Zookeeper等分布式協(xié)調(diào)服務(wù)來實(shí)現(xiàn)分布式鎖。
在使用悲觀鎖時(shí),需要注意死鎖和性能問題。死鎖是指兩個(gè)或多個(gè)線程無限期地等待對(duì)方釋放資源的情況。性能問題則可能由于鎖的粒度過大(如表級(jí)鎖)導(dǎo)致并發(fā)性能下降。
樂觀鎖與悲觀鎖的比較:
悲觀鎖:假設(shè)最壞的情況,每次訪問數(shù)據(jù)時(shí)都會(huì)鎖定數(shù)據(jù),防止其他事務(wù)修改。
樂觀鎖:假設(shè)最好的情況,允許其他事務(wù)并發(fā)訪問數(shù)據(jù),但在更新時(shí)會(huì)檢查數(shù)據(jù)是否被修改。
選擇哪種鎖策略取決于應(yīng)用的具體需求和并發(fā)場(chǎng)景。使用樂觀鎖時(shí),需要注意處理更新失敗的情況,通常是通過重試、拋出異?;蚪o用戶反饋來實(shí)現(xiàn)的。
遞歸鎖
Java中的遞歸鎖(ReentrantLock)是java.util.concurrent.locks包下提供的一種可重入的互斥鎖,它是悲觀鎖的一種實(shí)現(xiàn)。遞歸鎖允許一個(gè)線程多次獲取同一個(gè)鎖,而不會(huì)造成死鎖,這對(duì)于某些需要遞歸調(diào)用或者在一個(gè)線程中多次需要獲取同一個(gè)鎖的場(chǎng)景非常有用。
遞歸鎖的幾個(gè)特性:
可重入性:如果一個(gè)線程已經(jīng)擁有了一個(gè)遞歸鎖,那么它可以再次獲取該鎖而不會(huì)阻塞。每次獲取鎖,都會(huì)增加鎖的持有計(jì)數(shù);每次釋放鎖,都會(huì)減少持有計(jì)數(shù)。只有當(dāng)持有計(jì)數(shù)減少到0時(shí),其他線程才能獲取該鎖。
公平性:遞歸鎖可以是公平的也可以是非公平的。公平性意味著鎖的獲取是按照線程請(qǐng)求鎖的順序來的,而非公平性則不保證順序。公平的遞歸鎖可以減少“線程饑餓”的問題,但可能會(huì)降低性能。
既然我們說她是一個(gè)悲觀鎖的實(shí)現(xiàn),那么是不是可以和 synchronized 比較一下,有什么不同呢?
與Java內(nèi)置的synchronized關(guān)鍵字相比,遞歸鎖提供了更高的靈活性和更好的性能控制。例如,遞歸鎖支持嘗試獲取鎖(tryLock()方法)、定時(shí)獲取鎖(tryLock(long timeout, TimeUnit unit)方法)以及中斷等待鎖的線程(lockInterruptibly()方法)。
我們看一下遞歸鎖的示例代碼:
import java.util.concurrent.locks.ReentrantLock;
public class RecursiveLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 臨界區(qū)代碼
// ...
someNestedMethod();
// ...
} finally {
lock.unlock();
}
}
private void someNestedMethod() {
lock.lock();
try {
// 嵌套調(diào)用中需要同步的代碼
// ...
} finally {
lock.unlock();
}
}
}
在上面的示例中,someMethod方法調(diào)用了someNestedMethod方法,并且兩者都需要獲取同一個(gè)遞歸鎖。由于ReentrantLock是可重入的,所以這種調(diào)用不會(huì)造成死鎖。
讀寫鎖
Java中的讀寫鎖(ReadWriteLock)是一種允許多個(gè)讀線程和單個(gè)寫線程訪問共享資源的同步機(jī)制。ReadWriteLock接口在java.util.concurrent.locks包中定義,它包含兩個(gè)鎖:一個(gè)讀鎖和一個(gè)寫鎖。
讀寫鎖的特性:
讀共享:在沒有線程持有寫鎖時(shí),多個(gè)線程可以同時(shí)持有讀鎖來讀取共享資源。這可以提高并發(fā)性能,因?yàn)樽x操作通常不會(huì)修改數(shù)據(jù),所以允許多個(gè)讀線程并發(fā)訪問是安全的。
寫?yīng)氄迹寒?dāng)一個(gè)線程持有寫鎖時(shí),其他線程既不能獲取讀鎖也不能獲取寫鎖。這是為了確保寫操作對(duì)共享資源的獨(dú)占訪問,從而防止數(shù)據(jù)不一致。
Java中ReadWriteLock接口的主要實(shí)現(xiàn)類是ReentrantReadWriteLock,它提供了可重入的讀寫鎖實(shí)現(xiàn)。ReentrantReadWriteLock有兩個(gè)重要的方法:readLock()和writeLock(),分別用于獲取讀鎖和寫鎖。
我們看看示例代碼:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int data;
public void readData() {
lock.readLock().lock(); // 獲取讀鎖
try {
// 讀取共享資源
System.out.println("Reading data: " + data);
} finally {
lock.readLock().unlock(); // 釋放讀鎖
}
}
public void writeData(int newData) {
lock.writeLock().lock(); // 獲取寫鎖
try {
// 修改共享資源
this.data = newData;
System.out.println("Writing data: " + data);
} finally {
lock.writeLock().unlock(); // 釋放寫鎖
}
}
}
在這個(gè)例子中,readData方法使用讀鎖來讀取data字段,而writeData方法使用寫鎖來修改data字段。當(dāng)多個(gè)線程調(diào)用readData時(shí),它們可以同時(shí)讀取數(shù)據(jù)而不會(huì)相互阻塞,除非有一個(gè)線程正在調(diào)用writeData并持有寫鎖。
需要注意的是,ReentrantReadWriteLock還有一個(gè)構(gòu)造方法,它接受一個(gè)布爾值參數(shù)fair,用于指定鎖是否應(yīng)該是公平的。如果設(shè)置為true,則等待時(shí)間最長(zhǎng)的線程將優(yōu)先獲得鎖。但是,公平鎖可能會(huì)降低性能,因?yàn)樾枰S護(hù)一個(gè)有序的等待隊(duì)列。