Java鎖機制淺析:到底什么情況下該用ReentrantLock?
在多線程編程中,鎖(Lock)是一種重要的同步機制,它可以保證同一時間只有一個線程可以訪問共享資源。Java 中提供了兩種類型的鎖:隱式鎖和顯式鎖。
隱式鎖通過 synchronized 關(guān)鍵字實現(xiàn),在使用時比較方便,但其粒度較大,無法滿足復(fù)雜的同步需求。而顯式鎖則通過 Lock 接口實現(xiàn),可以更靈活地控制鎖的粒度和行為。本文將介紹 Java 顯式鎖中的顯示鎖(ReentrantLock)和顯示條件隊列(Condition),并討論它們的使用方法、進(jìn)階用法以及可能遇到的問題和解決方案。
一、顯示鎖
1、簡介
顯示鎖(ReentrantLock)是 Java 顯式鎖中最常用的一種,它實現(xiàn)了 Lock 接口的所有特性,并提供了可重入和公平性等額外功能。其中,可重入指同一線程可以多次獲取該鎖而不會造成死鎖,公平性指多個線程按照申請鎖的順序獲得鎖。
與隱式鎖不同的是,顯示鎖需要手動加鎖和釋放鎖,通常使用 try-finally 語句塊保證鎖的正確釋放,避免異常導(dǎo)致鎖未能被及時釋放而造成死鎖。
2、基本使用
顯示鎖(ReentrantLock)的基本用法如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void run() {
lock.lock(); // 加鎖
try {
count++; // 訪問共享資源
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們首先創(chuàng)建了一個 ReentrantLock 對象,并將其作為同步對象(Monitor)來訪問共享資源。然后,在訪問共享資源時使用 lock.lock() 方法加鎖,使用 lock.unlock() 方法解鎖。由于 lock 和 unlock 方法都可能拋出異常,因此通常需要使用 try-finally 語句塊來確保鎖的正確釋放。
3、可重入性
在 Java 中,可重入性指同一線程可以多次獲得該鎖而不會產(chǎn)生死鎖或排斥自己的情況。這是由于每個線程在加鎖時會記錄加鎖的次數(shù),只有在解鎖和加鎖次數(shù)相等時才真正釋放鎖。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void run() {
lock.lock(); // 第一次加鎖
try {
count++; // 訪問共享資源
lock.lock(); // 第二次加鎖
try {
count++; // 訪問共享資源
} finally {
lock.unlock(); // 第二次解鎖
}
} finally {
lock.unlock(); // 第一次解鎖
}
}
}
在上述示例中,我們先后兩次獲取了同一個鎖,并在其中訪問了共享資源。由于鎖是可重入的,因此即使在第二次加鎖時仍然持有鎖,也不會產(chǎn)生死鎖或排斥自己的情況。
4、公平性
在 Java 中,公平性指多個線程按照申請鎖的順序獲得鎖的特性。公平性可以避免某些線程長期持有鎖,導(dǎo)致其他線程無法獲得鎖而等待過長時間的情況。
在顯示鎖中,默認(rèn)情況下是非公平的,即當(dāng)前線程可以隨時獲得鎖,而不考慮其他線程的申請順序。這樣可能會導(dǎo)致某些線程一直無法獲得鎖,從而產(chǎn)生線程饑餓(Thread Starvation)的問題。
為了解決這個問題,Java 中提供了公平鎖(FairLock),它會按照線程申請鎖的順序進(jìn)行排隊,并且保證先來先得的原則。示例代碼如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock(true); // 公平鎖
private int count = 0;
public void run() {
lock.lock(); // 加鎖
try {
count++; // 訪問共享資源
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們創(chuàng)建了一個公平鎖(FairLock),并將其傳遞給 ReentrantLock 的構(gòu)造函數(shù)中。然后,在訪問共享資源時使用 lock.lock() 方法加鎖,使用 lock.unlock() 方法解鎖。由于公平鎖會按照線程申請鎖的順序進(jìn)行排隊,因此可以避免線程饑餓的問題。
二、顯示條件隊列
1、簡介
條件隊列(Condition)是 Java 顯式鎖中實現(xiàn)線程等待/通知機制的一種方式。它允許多個線程在某些條件不滿足時暫停執(zhí)行,并在特定條件滿足時恢復(fù)執(zhí)行。與 synchronized 關(guān)鍵字相比,條件隊列提供了更靈活和細(xì)粒度的同步控制,可以更好地支持復(fù)雜的同步需求。
條件隊列通常與顯示鎖一起使用,通過
ReentrantLock.newCondition() 方法創(chuàng)建一個 Condition 對象,并使用 await()、signal() 和 signalAll() 等方法來進(jìn)行線程等待和喚醒操作。其中,await() 方法用于使當(dāng)前線程等待某個條件發(fā)生變化,signal() 方法用于喚醒一個等待該條件的線程,signalAll() 方法用于喚醒所有等待該條件的線程。
2、基本使用
顯示條件隊列(Condition)的基本用法如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean flag = false;
public void run() {
lock.lock(); // 加鎖
try {
while (!flag) {
condition.await(); // 等待條件變化
}
// 訪問共享資源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 解鎖
}
}
public void changeFlag() {
lock.lock(); // 加鎖
try {
flag = true; // 修改條件
condition.signalAll(); // 喚醒等待的線程
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們首先創(chuàng)建了一個 Condition 對象,并將其關(guān)聯(lián)到一個顯示鎖(ReentrantLock)上。然后,在訪問共享資源時使用 while 循環(huán)判斷條件是否滿足,如果不滿足則調(diào)用 condition.await() 方法使當(dāng)前線程進(jìn)入等待狀態(tài)。在修改條件時調(diào)用 changeFlag() 方法,并使用 condition.signalAll() 喚醒所有等待該條件的線程。需要注意的是,await() 方法和 signal()/signalAll() 方法都必須在鎖保護(hù)下進(jìn)行調(diào)用,否則會拋出
IllegalMonitorStateException 異常。
3、進(jìn)階使用
條件隊列(Condition)還提供了許多高級操作,用于支持更復(fù)雜的同步需求。以下是一些常用的進(jìn)階使用方式:
(1)等待超時
有時候我們希望線程在等待一段時間后自動喚醒,而不是一直等待到被喚醒為止。這時候可以使用 condition.await(long time, TimeUnit unit) 方法,它允許我們指定等待的最長時間,如果超過指定時間仍未被喚醒,則自動退出等待狀態(tài)。示例代碼如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean flag = false;
public void run() {
lock.lock(); // 加鎖
try {
long timeout = 10L; // 等待 10 秒
while (!flag) {
if (!condition.await(timeout, TimeUnit.SECONDS)) {
// 在等待一定時間后還未被喚醒,做相應(yīng)處理
break;
}
}
// 訪問共享資源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 解鎖
}
}
public void changeFlag() {
lock.lock(); // 加鎖
try {
flag = true; // 修改條件
condition.signalAll(); // 喚醒等待的線程
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們使用 condition.await(timeout, TimeUnit.SECONDS) 方法等待了 10 秒,如果超過該時間還未被喚醒,則退出等待狀態(tài)并做相應(yīng)處理。
(2)等待多個條件
有時候我們需要等待多個條件同時滿足后才能繼續(xù)執(zhí)行,這時候可以使用多個條件隊列(Condition)來實現(xiàn)。示例代碼如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private boolean flag1 = false;
private boolean flag2 = false;
public void run() {
lock.lock(); // 加鎖
try {
while (!flag1 || !flag2) {
if (!flag1) {
condition1.await(); // 等待條件 1
}
if (!flag2) {
condition2.await(); // 等待條件 2
}
}
// 訪問共享資源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 解鎖
}
}
public void changeFlag1() {
lock.lock(); // 加鎖
try {
flag1 = true; // 修改條件 1
condition1.signalAll(); // 喚醒等待條件 1 的線程
} finally {
lock.unlock(); // 解鎖
}
}
public void changeFlag2() {
lock.lock(); // 加鎖
try {
flag2 = true; // 修改條件 2
condition2.signalAll(); // 喚醒等待條件 2 的線程
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們創(chuàng)建了兩個條件隊列(Condition),分別用于等待兩個不同的條件。然后,在訪問共享資源時使用 while 循環(huán)判斷兩個條件是否都滿足,如果不滿足則分別調(diào)用 condition1.await() 和 condition2.await() 方法使當(dāng)前線程進(jìn)入等待狀態(tài)。在修改條件時分別調(diào)用 changeFlag1() 和 changeFlag2() 方法,并使用 condition1.signalAll() 和 condition2.signalAll() 喚醒等待相應(yīng)條件的線程。
(3)實現(xiàn)生產(chǎn)者消費者模型
條件隊列(Condition)還可以用于實現(xiàn)生產(chǎn)者消費者模型,其中生產(chǎn)者和消費者共享一個緩沖區(qū),當(dāng)緩沖區(qū)為空時,消費者需要等待生產(chǎn)者生產(chǎn)數(shù)據(jù);當(dāng)緩沖區(qū)滿時,生產(chǎn)者需要等待消費者消費數(shù)據(jù)。示例代碼如下:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<Integer> queue = new LinkedList<>();
private final int maxSize = 10;
public void run() {
while (true) {
lock.lock(); // 加鎖
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待不為空
}
int data = queue.poll(); // 取出數(shù)據(jù)
notFull.signalAll(); // 喚醒生產(chǎn)者
// 處理數(shù)據(jù)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 解鎖
}
}
}
public void produce(int data) {
lock.lock(); // 加鎖
try {
while (queue.size() == maxSize) {
notFull.await(); // 等待不滿
}
queue.offer(data); // 添加數(shù)據(jù)
notEmpty.signalAll(); // 喚醒消費者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 解鎖
}
}
}
在上述示例中,我們創(chuàng)建了一個緩沖區(qū)(Queue),并使用兩個條件隊列(Condition)分別表示緩沖區(qū)不為空和不滿。在消費者線程中,使用 while 循環(huán)判斷緩沖區(qū)是否為空,如果為空則調(diào)用 notEmpty.await() 方法使當(dāng)前線程進(jìn)入等待狀態(tài)。當(dāng)從緩沖區(qū)取出數(shù)據(jù)后,調(diào)用 notFull.signalAll() 方法喚醒所有等待不滿的生產(chǎn)者線程。在生產(chǎn)者線程中,使用 while 循環(huán)判斷緩沖區(qū)是否已滿,如果已滿則調(diào)用 notFull.await() 方法使當(dāng)前線程進(jìn)入等待狀態(tài)。當(dāng)往緩沖區(qū)添加數(shù)據(jù)后,調(diào)用 notEmpty.signalAll() 方法喚醒所有等待不為空的消費者線程。
三、讀寫鎖
1、簡介
讀寫鎖是一種特殊的鎖,它允許多個線程同時讀取共享資源,但只允許一個線程對共享資源進(jìn)行寫操作。讀寫鎖可以有效地提高并發(fā)性能,特別是在讀取操作遠(yuǎn)多于寫操作的場景下。
Java 中提供了 ReentrantReadWriteLock 類來實現(xiàn)讀寫鎖。它包含一個讀鎖和一個寫鎖,讀鎖可同時被多個線程持有,但寫鎖一次只能被一個線程持有。示例代碼如下:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyRunnable implements Runnable {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int count = 0;
public void run() {
lock.readLock().lock(); // 獲取讀鎖
try {
// 訪問共享資源(讀取)
} finally {
lock.readLock().unlock(); // 釋放讀鎖
}
}
public void write() {
lock.writeLock().lock(); // 獲取寫鎖
try {
// 訪問共享資源(寫入)
} finally {
lock.writeLock().unlock(); // 釋放寫鎖
}
}
}
在上述示例中,我們創(chuàng)建了一個讀寫鎖(ReentrantReadWriteLock),并使用 readLock() 方法獲取讀鎖,writeLock() 方法獲取寫鎖。在訪問共享資源時,讀取操作可以同時被多個線程持有讀鎖,而寫入操作必須先獲取寫鎖,然后其他所有操作都被阻塞,直到寫入完成并釋放寫鎖。
2、使用場景
讀寫鎖適用于以下場景:
- 讀取操作遠(yuǎn)多于寫入操作。
- 共享資源的狀態(tài)不會發(fā)生太大變化,即讀取操作和寫入操作之間的時間間隔較長。
- 寫入操作對資源的一致性要求高,需要獨占式訪問。
使用讀寫鎖可以有效地提高程序的并發(fā)性能,特別是在讀取操作遠(yuǎn)多于寫入操作的情況下。但需要注意的是,讀寫鎖的實現(xiàn)需要消耗更多的系統(tǒng)資源,因此只有在讀取操作遠(yuǎn)多于寫入操作、且讀寫操作之間的時間間隔較長時才應(yīng)該使用讀寫鎖。
四、StampedLock
1、簡介
StampedLock 是 Java 8 新增的一種鎖機制,它是對讀寫鎖的一種改進(jìn),具有更高的并發(fā)性能。StampedLock 支持三種模式:讀(共享)、寫(獨占)和樂觀讀(非獨占)。與 ReadWriteLock 不同的是,StampedLock 的讀取操作不會被阻塞,但可能會失敗,如果讀取的數(shù)據(jù)在讀取過程中發(fā)生了改變,則讀取操作會失敗并返回一個標(biāo)記(stamp),此時可以根據(jù)需要重試讀取操作或者轉(zhuǎn)換為獨占寫入操作。
StampedLock 使用一個長整型的 stamp 來表示鎖的版本號,每次修改數(shù)據(jù)后都會更新版本號。讀取操作需要傳入當(dāng)前版本號以確保讀取的數(shù)據(jù)沒有被修改,寫入操作則需要傳入上一次讀取操作返回的版本號以確保數(shù)據(jù)的一致性。示例代碼如下:
import java.util.concurrent.locks.StampedLock;
public class MyRunnable implements Runnable {
private final StampedLock lock = new StampedLock();
private int x = 0;
private int y = 0;
public void run() {
long stamp = lock.tryOptimisticRead(); // 嘗試樂觀讀取
int currentX = x;
int currentY = y;
if (!lock.validate(stamp)) { // 校驗版本號
stamp = lock.readLock(); // 獲取讀鎖
try {
currentX = x; // 重新讀取數(shù)據(jù)
currentY = y;
} finally {
lock.unlockRead(stamp); // 釋放讀鎖
}
}
// 訪問共享資源(讀?。? }
public void write(int newX, int newY) {
long stamp = lock.writeLock(); // 獲取寫鎖
try {
x = newX; // 修改數(shù)據(jù)
y = newY;
} finally {
lock.unlockWrite(stamp); // 釋放寫鎖
}
}
}
在上述示例中,我們創(chuàng)建了一個 StampedLock,并使用 tryOptimisticRead() 方法嘗試進(jìn)行樂觀讀取操作。如果校驗版本號失敗,則說明數(shù)據(jù)被修改過,此時需要再次獲取讀鎖并重新讀取數(shù)據(jù)。在修改數(shù)據(jù)時,使用 writeLock() 方法獲取寫鎖,修改完成后釋放寫鎖。
2、使用場景
StampedLock 適用于以下場景:
- 讀取操作頻繁,而寫入操作較少。
- 數(shù)據(jù)的一致性要求不高,即數(shù)據(jù)會發(fā)生周期
性的變化,但讀取操作與寫入操作之間的時間間隔較短,不需要使用分布式鎖或者數(shù)據(jù)庫事務(wù)來保證數(shù)據(jù)一致性。
使用 StampedLock 可以提高程序的并發(fā)性能,特別是在讀取操作頻繁、寫入操作較少的情況下。但需要注意的是,StampedLock 的實現(xiàn)依賴于硬件的 CAS(Compare and Swap)指令,因此在某些 CPU 架構(gòu)上可能會存在性能問題。此外,在使用樂觀讀取模式時需要進(jìn)行版本號校驗,如果校驗失敗則需要重新獲取讀鎖并重新讀取數(shù)據(jù),這可能會帶來額外的開銷和復(fù)雜度。
五、總結(jié)
Java 提供了多種鎖機制來協(xié)調(diào)多個線程對共享資源的訪問。ReentrantLock 是最基本的一種鎖,它采用獨占式訪問方式,可以精確控制多個線程對共享資源的訪問順序。Condition 可以用于在鎖的基礎(chǔ)上實現(xiàn)更靈活的同步操作,例如線程的等待和喚醒。ReadWriteLock 是一種特殊的鎖,它允許多個線程同時讀取共享資源,但只允許一個線程對共享資源進(jìn)行寫操作。StampedLock 是對讀寫鎖的一種改進(jìn),具有更高的并發(fā)性能,但需要注意的是它的實現(xiàn)依賴于硬件的 CAS 指令。
在使用鎖時需要注意避免死鎖、避免過度競爭和防止資源饑餓等問題。應(yīng)該根據(jù)具體的場景選擇不同的鎖機制,并合理地設(shè)置鎖的粒度和范圍。同時也可以考慮使用一些高級的并發(fā)工具來簡化鎖的管理,例如 Executor 框架、原子變量、信號量、倒計時門閂等。