在多線程編程中,鎖(Lock)是一種重要的同步機制,它可以保證同一時間只有一個線程可以訪問共享資源。Java 中提供了兩種類型的鎖:隱式鎖和顯式鎖。
隱式鎖通過 synchronized 關鍵字實現(xiàn),在使用時比較方便,但其粒度較大,無法滿足復雜的同步需求。而顯式鎖則通過 Lock 接口實現(xiàn),可以更靈活地控制鎖的粒度和行為。本文將介紹 Java 顯式鎖中的顯示鎖(ReentrantLock)和顯示條件隊列(Condition),并討論它們的使用方法、進階用法以及可能遇到的問題和解決方案。
一、顯示鎖1、簡介顯示鎖(ReentrantLock)是 Java 顯式鎖中最常用的一種,它實現(xiàn)了 Lock 接口的所有特性,并提供了可重入和公平性等額外功能。其中,可重入指同一線程可以多次獲取該鎖而不會造成死鎖,公平性指多個線程按照申請鎖的順序獲得鎖。
(相關資料圖)
與隱式鎖不同的是,顯示鎖需要手動加鎖和釋放鎖,通常使用 try-finally 語句塊保證鎖的正確釋放,避免異常導致鎖未能被及時釋放而造成死鎖。
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 中,可重入性指同一線程可以多次獲得該鎖而不會產生死鎖或排斥自己的情況。這是由于每個線程在加鎖時會記錄加鎖的次數(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(); // 第一次解鎖 } }}
在上述示例中,我們先后兩次獲取了同一個鎖,并在其中訪問了共享資源。由于鎖是可重入的,因此即使在第二次加鎖時仍然持有鎖,也不會產生死鎖或排斥自己的情況。
4、公平性在 Java 中,公平性指多個線程按照申請鎖的順序獲得鎖的特性。公平性可以避免某些線程長期持有鎖,導致其他線程無法獲得鎖而等待過長時間的情況。
在顯示鎖中,默認情況下是非公平的,即當前線程可以隨時獲得鎖,而不考慮其他線程的申請順序。這樣可能會導致某些線程一直無法獲得鎖,從而產生線程饑餓(Thread Starvation)的問題。
為了解決這個問題,Java 中提供了公平鎖(FairLock),它會按照線程申請鎖的順序進行排隊,并且保證先來先得的原則。示例代碼如下:
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 的構造函數(shù)中。然后,在訪問共享資源時使用 lock.lock() 方法加鎖,使用 lock.unlock() 方法解鎖。由于公平鎖會按照線程申請鎖的順序進行排隊,因此可以避免線程饑餓的問題。
二、顯示條件隊列1、簡介條件隊列(Condition)是 Java 顯式鎖中實現(xiàn)線程等待/通知機制的一種方式。它允許多個線程在某些條件不滿足時暫停執(zhí)行,并在特定條件滿足時恢復執(zhí)行。與 synchronized 關鍵字相比,條件隊列提供了更靈活和細粒度的同步控制,可以更好地支持復雜的同步需求。
條件隊列通常與顯示鎖一起使用,通過ReentrantLock.newCondition() 方法創(chuàng)建一個 Condition 對象,并使用 await()、signal() 和 signalAll() 等方法來進行線程等待和喚醒操作。其中,await() 方法用于使當前線程等待某個條件發(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 對象,并將其關聯(lián)到一個顯示鎖(ReentrantLock)上。然后,在訪問共享資源時使用 while 循環(huán)判斷條件是否滿足,如果不滿足則調用 condition.await() 方法使當前線程進入等待狀態(tài)。在修改條件時調用 changeFlag() 方法,并使用 condition.signalAll() 喚醒所有等待該條件的線程。需要注意的是,await() 方法和 signal()/signalAll() 方法都必須在鎖保護下進行調用,否則會拋出IllegalMonitorStateException 異常。
3、進階使用條件隊列(Condition)還提供了許多高級操作,用于支持更復雜的同步需求。以下是一些常用的進階使用方式:
(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)) { // 在等待一定時間后還未被喚醒,做相應處理 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)并做相應處理。
(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)判斷兩個條件是否都滿足,如果不滿足則分別調用 condition1.await() 和 condition2.await() 方法使當前線程進入等待狀態(tài)。在修改條件時分別調用 changeFlag1() 和 changeFlag2() 方法,并使用 condition1.signalAll() 和 condition2.signalAll() 喚醒等待相應條件的線程。
(3)實現(xiàn)生產者消費者模型條件隊列(Condition)還可以用于實現(xiàn)生產者消費者模型,其中生產者和消費者共享一個緩沖區(qū),當緩沖區(qū)為空時,消費者需要等待生產者生產數(shù)據(jù);當緩沖區(qū)滿時,生產者需要等待消費者消費數(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 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(); // 喚醒生產者 // 處理數(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ū)是否為空,如果為空則調用 notEmpty.await() 方法使當前線程進入等待狀態(tài)。當從緩沖區(qū)取出數(shù)據(jù)后,調用 notFull.signalAll() 方法喚醒所有等待不滿的生產者線程。在生產者線程中,使用 while 循環(huán)判斷緩沖區(qū)是否已滿,如果已滿則調用 notFull.await() 方法使當前線程進入等待狀態(tài)。當往緩沖區(qū)添加數(shù)據(jù)后,調用 notEmpty.signalAll() 方法喚醒所有等待不為空的消費者線程。
三、讀寫鎖1、簡介讀寫鎖是一種特殊的鎖,它允許多個線程同時讀取共享資源,但只允許一個線程對共享資源進行寫操作。讀寫鎖可以有效地提高并發(fā)性能,特別是在讀取操作遠多于寫操作的場景下。
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、使用場景讀寫鎖適用于以下場景:
讀取操作遠多于寫入操作。共享資源的狀態(tài)不會發(fā)生太大變化,即讀取操作和寫入操作之間的時間間隔較長。寫入操作對資源的一致性要求高,需要獨占式訪問。使用讀寫鎖可以有效地提高程序的并發(fā)性能,特別是在讀取操作遠多于寫入操作的情況下。但需要注意的是,讀寫鎖的實現(xiàn)需要消耗更多的系統(tǒng)資源,因此只有在讀取操作遠多于寫入操作、且讀寫操作之間的時間間隔較長時才應該使用讀寫鎖。
四、StampedLock1、簡介StampedLock 是 Java 8 新增的一種鎖機制,它是對讀寫鎖的一種改進,具有更高的并發(fā)性能。StampedLock 支持三種模式:讀(共享)、寫(獨占)和樂觀讀(非獨占)。與 ReadWriteLock 不同的是,StampedLock 的讀取操作不會被阻塞,但可能會失敗,如果讀取的數(shù)據(jù)在讀取過程中發(fā)生了改變,則讀取操作會失敗并返回一個標記(stamp),此時可以根據(jù)需要重試讀取操作或者轉換為獨占寫入操作。
StampedLock 使用一個長整型的 stamp 來表示鎖的版本號,每次修改數(shù)據(jù)后都會更新版本號。讀取操作需要傳入當前版本號以確保讀取的數(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() 方法嘗試進行樂觀讀取操作。如果校驗版本號失敗,則說明數(shù)據(jù)被修改過,此時需要再次獲取讀鎖并重新讀取數(shù)據(jù)。在修改數(shù)據(jù)時,使用 writeLock() 方法獲取寫鎖,修改完成后釋放寫鎖。
2、使用場景StampedLock 適用于以下場景:
讀取操作頻繁,而寫入操作較少。數(shù)據(jù)的一致性要求不高,即數(shù)據(jù)會發(fā)生周期性的變化,但讀取操作與寫入操作之間的時間間隔較短,不需要使用分布式鎖或者數(shù)據(jù)庫事務來保證數(shù)據(jù)一致性。
使用 StampedLock 可以提高程序的并發(fā)性能,特別是在讀取操作頻繁、寫入操作較少的情況下。但需要注意的是,StampedLock 的實現(xiàn)依賴于硬件的 CAS(Compare and Swap)指令,因此在某些 CPU 架構上可能會存在性能問題。此外,在使用樂觀讀取模式時需要進行版本號校驗,如果校驗失敗則需要重新獲取讀鎖并重新讀取數(shù)據(jù),這可能會帶來額外的開銷和復雜度。
五、總結Java 提供了多種鎖機制來協(xié)調多個線程對共享資源的訪問。ReentrantLock 是最基本的一種鎖,它采用獨占式訪問方式,可以精確控制多個線程對共享資源的訪問順序。Condition 可以用于在鎖的基礎上實現(xiàn)更靈活的同步操作,例如線程的等待和喚醒。ReadWriteLock 是一種特殊的鎖,它允許多個線程同時讀取共享資源,但只允許一個線程對共享資源進行寫操作。StampedLock 是對讀寫鎖的一種改進,具有更高的并發(fā)性能,但需要注意的是它的實現(xiàn)依賴于硬件的 CAS 指令。
在使用鎖時需要注意避免死鎖、避免過度競爭和防止資源饑餓等問題。應該根據(jù)具體的場景選擇不同的鎖機制,并合理地設置鎖的粒度和范圍。同時也可以考慮使用一些高級的并發(fā)工具來簡化鎖的管理,例如 Executor 框架、原子變量、信號量、倒計時門閂等。
標簽: