您好,登錄后才能下訂單哦!
本篇文章為大家展示了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則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
想到鎖我們一般想到的是同步鎖即 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是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包下的源碼進行分析。
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插隊了。
通過執行結果可以看出 公平鎖和非公平鎖的區別,公平鎖能保證等待線程優先執行,但是非公平鎖可能會被其它線程插隊。
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實現銀行存取款,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。