一、樂觀鎖(Optimistic Locking)
- 原理:樂觀鎖假設(shè)在大多數(shù)情況下,多個線程之間不會發(fā)生沖突。在讀取數(shù)據(jù)時,每個線程會獲得一個標(biāo)識符(如版本號或時間戳)。在提交修改之前,會比較當(dāng)前標(biāo)識符與之前讀取的標(biāo)識符是否相等,如果相等則提交成功,否則說明數(shù)據(jù)已被其他線程修改,需要進行沖突處理。
- 實現(xiàn)方式:通常使用版本號或時間戳來實現(xiàn),可以在數(shù)據(jù)庫中添加一個額外的字段作為標(biāo)識符,并在更新操作時進行比較。
- 應(yīng)用場景:適用于讀操作頻繁而寫操作較少的場景,可以減少鎖的使用,提高并發(fā)性能。
代碼示例
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private static AtomicInteger version = new AtomicInteger(0);
private static int sharedData = 0;
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
int currentVersion = version.get(); // 讀取當(dāng)前版本號
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 檢查版本號是否仍為之前讀取的版本號
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} else {
System.out.println("Thread 1: Failed to update shared data due to concurrent modification");
}
});
Thread thread2 = new Thread(() -> {
int currentVersion = version.get(); // 讀取當(dāng)前版本號
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 檢查版本號是否仍為之前讀取的版本號
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} else {
System.out.println("Thread 2: Failed to update shared data due to concurrent modification");
}
});
thread1.start();
thread2.start();
}
}
這個例子中,我們使用了 AtomicInteger 類的 compareAndSet() 方法來實現(xiàn)樂觀鎖。首先,我們定義了一個版本號 version 用于追蹤共享數(shù)據(jù)的變化。然后,我們創(chuàng)建了兩個線程,每個線程都讀取當(dāng)前版本號并對共享數(shù)據(jù)執(zhí)行修改操作。在提交修改之前,線程會再次檢查當(dāng)前版本號是否仍為之前讀取的版本號,如果是,則提交修改成功;否則,說明數(shù)據(jù)已被其他線程修改,需要進行相應(yīng)處理。
二、悲觀鎖(Pessimistic Locking)
- 原理:悲觀鎖假設(shè)在多線程環(huán)境下,對共享資源的訪問會產(chǎn)生沖突,因此默認(rèn)認(rèn)為每次訪問都會發(fā)生沖突,需要加鎖保證獨占訪問。
- 實現(xiàn)方式:可以使用synchronized關(guān)鍵字或Lock接口的具體實現(xiàn)(如ReentrantLock)來實現(xiàn)。
- 應(yīng)用場景:適用于寫操作頻繁的場景,因為它能夠確保數(shù)據(jù)一致性和線程安全。
代碼示例
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
thread1.start();
thread2.start();
}
}
在這個示例中,我們使用了 ReentrantLock 類來實現(xiàn)悲觀鎖。首先,我們創(chuàng)建了一個名為 lock 的 ReentrantLock 對象來保護共享數(shù)據(jù)。然后,我們創(chuàng)建了兩個線程,在涉及到共享數(shù)據(jù)的代碼塊中分別調(diào)用 lock() 方法獲取鎖,并在修改共享數(shù)據(jù)后調(diào)用 unlock() 方法釋放鎖。
在這個示例中,使用悲觀鎖的方式是通過顯式地獲取和釋放鎖來實現(xiàn)的。當(dāng)一個線程獲取到鎖時,其他線程會被阻塞,直到鎖被釋放。這樣可以確保同一時間只有一個線程能夠訪問共享資源,保證了數(shù)據(jù)的一致性和線程安全性。
三、可重入鎖(Reentrant Lock)
- 原理:可重入鎖是一種特殊類型的鎖,允許同一個線程多次獲得鎖,也稱為可重入性。當(dāng)一個線程已經(jīng)持有鎖時,再次請求獲取該鎖是允許的,而不會導(dǎo)致線程被阻塞,這種機制可以避免死鎖。
- 實現(xiàn)方式:在Java中,ReentrantLock類和synchronized關(guān)鍵字都是可重入鎖的實現(xiàn)。
- 應(yīng)用場景:適用于某個線程需要遞歸地調(diào)用同步方法或代碼塊的場景,提高代碼的靈活性。
代碼示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
updateSharedData(); // 調(diào)用可重入方法
} finally {
lock.unlock(); // 釋放鎖
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
updateSharedData(); // 調(diào)用可重入方法
} finally {
lock.unlock(); // 釋放鎖
}
});
thread1.start();
thread2.start();
}
private static void updateSharedData() {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Shared data updated inside the reentrant method to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
}
}
在這個示例中,我們使用了 ReentrantLock 類來實現(xiàn)可重入鎖。首先,我們創(chuàng)建了一個名為 lock 的 ReentrantLock 對象來保護共享數(shù)據(jù)。然后,我們創(chuàng)建了兩個線程,在涉及到共享數(shù)據(jù)的代碼塊中分別調(diào)用 lock() 方法獲取鎖,并在修改共享數(shù)據(jù)后調(diào)用 unlock() 方法釋放鎖。
值得注意的是,可重入鎖允許同一個線程多次獲取鎖。在示例中,當(dāng)線程1獲取到鎖后,在修改共享數(shù)據(jù)期間又調(diào)用了 updateSharedData() 方法,該方法中也需要獲取鎖。由于可重入鎖的特性,線程1可以再次獲取鎖,而不會造成死鎖。
可重入鎖在Java中有多種實現(xiàn)方式,其中最常見的是 ReentrantLock 類??芍厝腈i提供了一種靈活且強大的機制,用于管理并保護共享資源的訪問。
四、公平鎖(Fair Lock)
- 原理:公平鎖是一種保證線程獲取鎖的順序與其申請鎖的順序相同的鎖機制。它會按照線程的申請順序來分配鎖資源,避免某個線程饑餓地等待鎖。
- 實現(xiàn)方式:可以使用ReentrantLock類的構(gòu)造函數(shù)指定為公平鎖。
- 應(yīng)用場景:當(dāng)多個線程競爭同一個資源時,希望公平地分配鎖資源,避免某個線程長時間無法獲取到鎖的場景。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock(true); // 創(chuàng)建公平鎖
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
thread1.start();
thread2.start();
}
}
在這個示例中,我們使用了 ReentrantLock 類來實現(xiàn)公平鎖。通過在創(chuàng)建 ReentrantLock 對象時傳遞參數(shù) true,我們創(chuàng)建了一個公平鎖,即等待時間最長的線程會最先獲取到鎖。
在公平鎖中,當(dāng)多個線程競爭同一個鎖時,鎖會按照線程等待的順序分配給它們。這可以確保較早等待的線程優(yōu)先獲得鎖,避免了饑餓情況的發(fā)生,即某些線程一直無法獲得鎖。
需要注意的是,公平鎖可能會犧牲一定的性能,因為它需要維護一個隊列來管理等待的線程。因此,當(dāng)性能要求較高且沒有特殊需求時,可以使用非公平鎖。
在實際開發(fā)中,公平鎖的選擇應(yīng)根據(jù)具體的業(yè)務(wù)需求和性能要求綜合考慮。
五、互斥鎖(Mutex)
- 原理:互斥鎖是一種用于保護共享資源不被并發(fā)訪問的鎖機制。它通過對代碼塊或方法進行加鎖,確保同一時刻只有一個線程能夠執(zhí)行被保護的代碼。
- 實現(xiàn)方式:在Java中,可以使用synchronized關(guān)鍵字或ReentrantLock類來實現(xiàn)互斥鎖。
- 應(yīng)用場景:適用于需要保護臨界區(qū)的代碼,確保數(shù)據(jù)一致性和線程安全的場景。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexLockExample {
private static int sharedData = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 獲取鎖
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 釋放鎖
}
});
thread1.start();
thread2.start();
}
}
在這個示例中,我們使用了 ReentrantLock 類來實現(xiàn)互斥鎖。通過創(chuàng)建一個 ReentrantLock 對象,我們獲得了一個可重入鎖。這意味著同一個線程可以多次獲取同一個鎖而不會發(fā)生死鎖。
在示例中,當(dāng)一個線程獲取到鎖后,其他線程將被阻塞直到鎖被釋放。這樣確保了同時只有一個線程能夠修改共享數(shù)據(jù),從而避免了數(shù)據(jù)競爭和并發(fā)問題。
需要注意的是,在使用互斥鎖時,務(wù)必在合適的地方調(diào)用 unlock() 方法來釋放鎖,以避免死鎖和資源泄漏。
互斥鎖是一種常見且有效的保護共享資源的機制,在并發(fā)編程中被廣泛使用。使用互斥鎖可以確保共享數(shù)據(jù)的一致性和線程安全。
六、自旋鎖(Spin Lock)
- 原理:自旋鎖是一種忙等待的鎖機制,在線程嘗試獲得鎖時不會立即阻塞,而是循環(huán)檢測鎖的狀態(tài),直到成功獲取鎖或達(dá)到最大自旋次數(shù)。
- 實現(xiàn)方式:在Java中,可以使用AtomicInteger類的compareAndSet方法來實現(xiàn)簡單的自旋鎖。
- 應(yīng)用場景:適用于鎖保持時間非常短暫的情況,避免線程頻繁地阻塞和喚醒。
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLockExample {
private static int sharedData = 0;
private static AtomicBoolean lock = new AtomicBoolean(false);
public static void main(String[] args) {
// 創(chuàng)建兩個線程并啟動
Thread thread1 = new Thread(() -> {
while (!lock.compareAndSet(false, true)) {
// 自旋等待鎖釋放
}
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.set(false); // 釋放鎖
}
});
Thread thread2 = new Thread(() -> {
while (!lock.compareAndSet(false, true)) {
// 自旋等待鎖釋放
}
try {
int newValue = sharedData + 1; // 對共享數(shù)據(jù)進行修改
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.set(false); // 釋放鎖
}
});
thread1.start();
thread2.start();
}
}
在這個示例中,我們使用了 AtomicBoolean 類來實現(xiàn)自旋鎖。自旋鎖是一種在獲取鎖時反復(fù)檢查鎖狀態(tài)的鎖機制。
在示例中,每個線程使用 compareAndSet 方法來嘗試獲取鎖。如果鎖當(dāng)前的狀態(tài)是未鎖定(false),則將其設(shè)置為鎖定(true),從而成功獲取到鎖。如果鎖已經(jīng)被其他線程獲取,則會一直進行自旋等待,直到鎖被釋放。
需要注意的是,在使用自旋鎖時,要避免死鎖和活鎖的情況發(fā)生。因此,合理選擇自旋次數(shù)和自旋等待時間非常重要。過長的自旋時間可能導(dǎo)致性能下降,而過短的自旋時間可能導(dǎo)致過多的線程切換開銷。
自旋鎖適用于對共享數(shù)據(jù)的訪問時間較短,且競爭不是很激烈的場景。在高并發(fā)情況下,自旋鎖可能會導(dǎo)致CPU資源的浪費,因此需要根據(jù)具體業(yè)務(wù)場景和性能要求綜合考慮是否使用自旋鎖。
七、閉鎖(Latch)
- 原理:閉鎖是一種用于等待其他線程完成操作的同步工具。它允許一個或多個線程等待其他線程執(zhí)行完特定任務(wù)后再繼續(xù)執(zhí)行。
- 實現(xiàn)方式:在Java中,CountDownLatch和CyclicBarrier是常見的閉鎖實現(xiàn)。
- 應(yīng)用場景:適用于需要等待其他線程完成某個任務(wù)后再繼續(xù)執(zhí)行的場景。
import java.util.concurrent.CountDownLatch;
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3; // 工作線程數(shù)目
CountDownLatch latch = new CountDownLatch(workerCount);
// 創(chuàng)建工作線程并啟動
for (int i = 0; i < workerCount; i++) {
Thread thread = new Thread(() -> {
// 模擬工作
System.out.println("Worker thread start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker thread finish");
latch.countDown(); // 工作完成,計數(shù)減一
});
thread.start();
}
System.out.println("Main thread waiting for workers to finish");
latch.await(); // 主線程等待所有工作線程完成
System.out.println("All workers have finished");
// 繼續(xù)主線程的后續(xù)操作
}
}
在這個示例中,我們使用了 CountDownLatch 類來實現(xiàn)閉鎖。閉鎖是一種同步工具,它可以使一個或多個線程等待其他線程完成某些操作后再繼續(xù)執(zhí)行。
在示例中,主線程首先創(chuàng)建了一個 CountDownLatch 對象,并指定需要等待的工作線程數(shù)目為 workerCount。然后,主線程創(chuàng)建了多個工作線程,并在每個工作線程開始和結(jié)束時調(diào)用 countDown() 方法,表示工作完成。
主線程在調(diào)用 await() 方法后會被阻塞,直到計數(shù)器減至零,即所有工作線程都完成了工作。然后,主線程可以繼續(xù)執(zhí)行接下來的操作。
閉鎖適用于一組線程需要等待某個條件滿足后再同時繼續(xù)執(zhí)行的場景。通過閉鎖,可以更好地控制線程的并發(fā)執(zhí)行。
八、信號量(Semaphore)
- 原理:信號量是一種用于控制同時訪問某個資源的線程數(shù)的同步工具。它可以指定能同時訪問資源的線程個數(shù),并提供了獲取和釋放許可的機制。
- 實現(xiàn)方式:在Java中,Semaphore類是信號量的實現(xiàn)。
- 應(yīng)用場景:適用于需要限制并發(fā)訪問某個資源的線程數(shù)或控制流量的場景。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int workerCount = 5; // 工作線程數(shù)目
Semaphore semaphore = new Semaphore(2); // 信號量,初始許可證數(shù)量為2
// 創(chuàng)建工作線程并啟動
for (int i = 0; i < workerCount; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire(); // 獲取許可證,如果沒有可用的許可證,則阻塞等待
System.out.println("Worker thread start");
// 模擬工作
Thread.sleep(1000);
System.out.println("Worker thread finish");
semaphore.release(); // 釋放許可證
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在這個示例中,我們使用了 Semaphore 類來實現(xiàn)信號量。信號量是一種同步工具,它可以控制對某個資源的訪問數(shù)量。
在示例中,創(chuàng)建了一個初始許可證數(shù)量為2的信號量(semaphore)。然后,創(chuàng)建了多個工作線程并啟動。
每個工作線程在開始工作之前調(diào)用 acquire() 方法來獲取許可證。如果當(dāng)前有可用的許可證,則線程獲取到許可證并繼續(xù)執(zhí)行工作。如果當(dāng)前沒有可用的許可證,則線程會阻塞等待,直到有其他線程釋放許可證。
工作線程完成工作后調(diào)用 release() 方法來釋放許可證,使得其他等待的線程可以獲取許可證繼續(xù)執(zhí)行工作。
通過信號量,我們可以控制同時訪問某個資源的線程數(shù)量,實現(xiàn)對并發(fā)訪問的控制和限制。