共計 6593 個字符,預計需要花費 17 分鐘才能閱讀完成。
行業資訊
服務器
云計算
java 并發編程中如何通過 ReentrantLock 和 Condition 實現銀行存取款
本篇文章為大家展示了 java 并發編程中如何通過 ReentrantLock 和 Condition 實現銀行存取款,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
java.util.concurrent.locks 包為鎖和等待條件提供一個框架的接口和類,它不同于內置同步和監視器。該框架允許更靈活地使用鎖和條件,但以更難用的語法為代價。
Lock 接口支持那些語義不同(重入、公平等)的鎖規則,可以在非阻塞式結構的上下文(包括 hand-over-hand 和鎖重排算法)中使用這些規則。主要的實現是 ReentrantLock。
ReadWriteLock 接口以類似方式定義了一些讀取者可以共享而寫入者獨占的鎖。此包只提供了一個實現,即 ReentrantReadWriteLock,因為它適用于大部分的標準用法上下文。但程序員可以創建自己的、適用于非標準要求的實現。
以下是 locks 包的相關類圖:
在之前我們同步一段代碼或者對象時都是使用 synchronized 關鍵字,使用的是 Java 語言的內置特性,然而 synchronized 的特性也導致了很多場景下出現問題,比如:
在一段同步資源上,首先線程 A 獲得了該資源的鎖,并開始執行,此時其他想要操作此資源的線程就必須等待。如果線程 A 因為某些原因而處于長時間操作的狀態,比如等待網絡,反復重試等等。那么其他線程就沒有辦法及時的處理它們的任務,只能無限制的等待下去。如果線程 A 的鎖在持有一段時間后可自動被釋放,那么其他線程不就可以使用該資源了嗎?再有就是類似于數據庫中的共享鎖與排它鎖,是否也可以應用到應用程序中?所以引入 Lock 機制就可以很好的解決這些問題。
Lock 提供了比 synchronized 更多的功能。但是要注意以下幾點:
? Lock 不是 Java 語言內置的,synchronized 是 Java 語言的關鍵字,因此是內置特性。Lock 是一個類,通過這個類可以實現同步訪問;
? Lock 和 synchronized 有一點非常大的不同,采用 synchronized 不需要用戶去手動釋放鎖,當 synchronized 方法或者 synchronized 代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而 Lock 則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
一、可重入鎖 ReentrantLock
想到鎖我們一般想到的是同步鎖即 Synchronized, 這里介紹的可重入鎖 ReentrantLock 的效率更高。IBM 對于可重入鎖進行了一個介紹:JDK 5.0 中更靈活、更具可伸縮性的鎖定機制
這里簡單介紹下可重入鎖的分類:(假設線程 A 獲取了鎖,現在 A 執行完成了,釋放了鎖同時喚醒了正在等待被喚醒的線程 B。但是,A 執行喚醒操作到 B 真正獲取鎖的時間里可能存在線程 C 已經獲取了鎖,造成正在排隊等待的 B 無法獲得鎖)
1) 公平鎖:
由于 B 先在等待被喚醒,為了保證公平性原則,公平鎖會先讓 B 獲得鎖。
2) 非公平鎖
不保證 B 先獲取到鎖對象。
這兩種鎖只要在構造 ReentrantLock 對象時加以區分就可以了,當參數設置為 true 時為公平鎖,false 時為非公平鎖,同時默認構造函數也是創建了一個非公平鎖。
private Lock lock = new ReentrantLock(true); ReentrantLock 的公平鎖在性能和實效性上作了很大的犧牲,可以參考 IBM 上發的那篇文章中的說明。
二、條件變量 Condition
Condition 是 java.util.concurrent.locks 包下的一個接口, Condition 接口描述了可能會與鎖有關聯的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關聯。為了避免兼容性問題,Condition 方法的名稱與對應的 Object 版本中的不同。
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,為每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。
Condition(也稱為條件隊列 或條件變量)為線程提供了一種手段,在某個狀態條件下直到接到另一個線程的通知,一直處于掛起狀態(即“等待”)。因為訪問此共享狀態信息發生在不同的線程中,所以它必須受到保護,因此要將某種形式的鎖與 Condition 相關聯。
Condition 實例實質上被綁定到一個鎖上。
這里不再對 Locks 包下的源碼進行分析。
三、ReentrantLock 和 Condition 設計多線程存取款
1. 存款的時候,不能有線程在取款。取款的時候,不能有線程在存款。
2. 取款時,余額大于取款金額才能進行取款操作,否則提示余額不足。
3. 當取款時,如果金額不足,則阻塞當前線程,并等待 2s(可能有其他線程將錢存入)。
如果 2s 之內沒有其它線程完成存款,或者還是金額不足則打印金額不足。
如果其它存入足夠金額則通知該阻塞線程,并完成取款操作。
/**
* 普通銀行賬戶,不可透支
*/
public class MyCount {
private String oid; // 賬號
private int cash; // 賬戶余額
// 賬戶鎖,這里采用公平鎖,掛起的取款線程優先獲得鎖,而不是讓其它存取款線程獲得鎖
private Lock lock = new ReentrantLock(true);
private Condition _save = lock.newCondition(); // 存款條件
private Condition _draw = lock.newCondition(); // 取款條件
MyCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
/**
* 存款
* @param x 操作金額
* @param name 操作人
*/
public void saving(int x, String name) { lock.lock(); // 獲取鎖
if (x 0) {
cash += x; // 存款
System.out.println(name + 存款 + x + ,當前余額為 + cash);
}
_draw.signalAll(); // 喚醒所有等待線程。 lock.unlock(); // 釋放鎖
}
/**
* 取款
* @param x 操作金額
* @param name 操作人
*/
public void drawing(int x, String name) { lock.lock(); // 獲取鎖
try { if (cash - x 0) {
System.out.println(name + 阻塞中
_draw.await(2000,TimeUnit.MILLISECONDS); // 阻塞取款操作, await 之后就隱示自動釋放了 lock,直到被喚醒自動獲取
}
if(cash-x =0){
cash -= x; // 取款
System.out.println(name + 取款 + x + ,當前余額為 + cash);
}else{ System.out.println(name+ 余額不足, 當前余額為 +cash+ 取款金額為 +x);
}
// 喚醒所有存款操作,這里并沒有什么實際作用,因為存款代碼中沒有阻塞的操作
_save.signalAll();
} catch (InterruptedException e) { e.printStackTrace();
} finally { lock.unlock(); // 釋放鎖
}
}
}
這里的可重入鎖也可以設置成非公平鎖,這樣阻塞取款線程可能后與其它存取款操作。
/**
* 存款線程類
*/
static class SaveThread extends Thread {
private String name; // 操作人
private MyCount myCount; // 賬戶
private int x; // 存款金額
SaveThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() { myCount.saving(x, name);
}
}
/**
* 取款線程類
*/
static class DrawThread extends Thread {
private String name; // 操作人
private MyCount myCount; // 賬戶
private int x; // 存款金額
DrawThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() { myCount.drawing(x, name);
}
}
public static void main(String[] args) {
// 創建并發訪問的賬戶
MyCount myCount = new MyCount(95599200901215522 , 1000);
// 創建一個線程池
ExecutorService pool = Executors.newFixedThreadPool(3);
Thread t1 = new SaveThread(S1 , myCount, 100);
Thread t2 = new SaveThread(S2 , myCount, 1000);
Thread t3 = new DrawThread(D1 , myCount, 12600);
Thread t4 = new SaveThread(S3 , myCount, 600);
Thread t5 = new DrawThread(D2 , myCount, 2300);
Thread t6 = new DrawThread(D3 , myCount, 1800);
Thread t7 = new SaveThread(S4 , myCount, 200);
// 執行各個線程
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
pool.execute(t7);
try { Thread.sleep(3000);
} catch (InterruptedException e) { e.printStackTrace();
}
// 關閉線程池
pool.shutdown();
}
}
上述類中定義了多個存取款的線程,執行結果如下:
S1 存款 100,當前余額為 1100
S3 存款 600,當前余額為 1700
D2 阻塞中
S2 存款 1000,當前余額為 2700
D2 取款 2300,當前余額為 400
D3 阻塞中
S4 存款 200,當前余額為 600
D3 余額不足, 當前余額為 600 取款金額為 1800
D1 阻塞中
D1 余額不足, 當前余額為 600 取款金額為 12600
執行步驟如下:
初始化賬戶,有余額 100。
S1,S3 完成存款。
D2 取款,余額不足,釋放鎖并阻塞線程,進入等待隊列中。
S2 完成存款操作后,會喚醒掛起的線程,這時 D2 完成了取款。
D3 取款,余額不足,釋放鎖并阻塞線程,進入等待隊列中。
S4 完成存款操作后,喚醒 D3,但是依然余額不足,D3 取款失敗。
D1 進行取款,等待 2s 鐘,無任何線程將其喚醒,取款失敗。
這里需要注意的是,當 Condition 調用 await() 方法時,當前線程會釋放鎖(否則就和 Sychnize 就沒有區別了)
將銀行賬戶中的 鎖改成非公平鎖時,執行的結果如下:
1 存款 100,當前余額為 1100
S3 存款 600,當前余額為 1700
D2 阻塞中
S2 存款 1000,當前余額為 2700
D3 取款 1800,當前余額為 900
D2 余額不足, 當前余額為 900 取款金額為 2300
S4 存款 200,當前余額為 1100
D1 阻塞中
D1 余額不足, 當前余額為 1100 取款金額為 12600
D2 取款出現余額不足后釋放鎖,進入等待狀態。但是當 S2 線程完成存款后并沒有立刻執行 D2 線程,而是被 D3 插隊了。
通過執行結果可以看出 公平鎖和非公平鎖的區別,公平鎖能保證等待線程優先執行,但是非公平鎖可能會被其它線程插隊。
四、ArrayBlockingQueue 中關于 ReentrantLock 和 Condition 的應用
JDK 源碼中關于可重入鎖的非常典型的應用是 BlockingQueue,從它的源碼中的成員變量大概就能知道了(ArrayBlockingQueue 為例):
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
// 主要解決多線程訪問的線程安全性問題
final ReentrantLock lock;
/** Condition for waiting takes */
// 添加元素時,通過 notEmpty 喚醒消費線程(在等待該條件) private final Condition notEmpty;
/** Condition for waiting puts */
// 刪除元素時,通過 notFull 喚醒生成線程(在等待該條件) private final Condition notFull;
ArrayBlockingQueue 是一個典型的生產者消費者模型,通過一個數組保存元素。為了保證添加和刪除元素的線程安全性,增加了可重入鎖和條件變量。
可重入鎖主要保證多線程對阻塞隊列的操作是線程安全的,同時為了讓被阻塞的消費者或者生產者能夠被自動喚醒,這里引入了條件變量。
當隊列已滿時,Producer 會被阻塞,此時如果 Customer 消費一個元素時,被阻塞的 Producer 就會被自動喚醒并往隊列中添加元素。
上面的兩個例子可見 java.util.concurrent.locks 包下的 ReentrantLock 和 Condition 配合起來的靈活性及實用性。
上述內容就是 java 并發編程中如何通過 ReentrantLock 和 Condition 實現銀行存取款,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注丸趣 TV 行業資訊頻道。