653
技術社區[雲棲]
銀行存取款模型的線程同步問題
關於線程同步,網上也有很多資料,不過不同的人理解也不大一樣,最近在研究這個問題的時候回想起大學課本上的一個經典模型,即銀行存取款模型,通過這個模型,我個人感覺解釋起來還是比較清楚的。本文結合自己的思考對該模型進行一個簡單的模擬,闡述一下我對線程同步的理解。
場景模擬
接下來使用java對該問題進行模擬。在研究這個問題時會忽略掉現實係統中的很多其他屬性,通過一個最簡單的餘額問題來看線程同步,這裏首先創建三個類。
1.卡類,同時卡類提供三個方法,獲取餘額、存款以及取款。
public class Card {
/*餘額初始化*/
private double balance;
public Card(double balance){
this.balance = balance;
}
/*獲取餘額方法*/
public double Get_balance(){
return this.balance;
}
/*存款方法*/
public void deposit(double count) throws InterruptedException{
System.out.println("存錢線程:存入金額=" + count);
double now = balance + count;
balance = now;
System.out.println("存錢線程:當前金額=" + balance);
}
/*取款方法*/
public void withdraw(double count) throws InterruptedException{
System.out.println("取錢線程:取出金額=" + count);
double now = balance - count;
balance = now;
System.out.println("取錢線程:當前金額=" + balance);
}
}
然後是兩個線程類,用於模擬並發操作所引入的餘額問題。
2.存款線程類,存入金額100。
public class DepositThread extends Thread{
private Card card;
public DepositThread(Card card){
this.card = card;
}
@Override
public void run(){
try {
card.deposit(100);
}
catch(Exception e){System.out.println(e.toString());}
}
}
3.取款線程類,取出金額50。
public class WithdrawThread extends Thread{
private Card card;
public WithdrawThread(Card card){
this.card = card;
}
@Override
public void run(){
try {
card.withdraw(50);
}
catch(Exception e){
System.out.println(e.toString());
}
}
}
現在先進行一個測試,讓存款線程先進行存錢操作,然後取款線程進行取款,最後驗證餘額與邏輯是否符合。
測試代碼如下:
public class CardTest{
public static void main(String[] args) throws InterruptedException{
Card card = new Card(100);
System.out.println("操作前餘額:" + card.Get_balance());
DepositThread depositThread = new DepositThread(card);
WithdrawThread withdrawThread = new WithdrawThread(card);
depositThread.start();
withdrawThread.start();
Thread.sleep(2000);
System.out.println("最終餘額:" + card.Get_balance());
}
}
現在大致的看一下,初始餘額為100,然後存款線程存入100,接下來取款線程取走50,那麼最後餘額為150。這麼看來,貌似沒問題?
數據不一致問題
事實上,存取款過程是需要消耗時間的,隻要一個線程在操作餘額期間受到其他線程的幹擾,就可能出現數據不一致問題。這裏我們修改存取款方法的代碼如下。
存款方法:
public void deposit(double count) throws InterruptedException{
System.out.println("存錢線程:存入金額=" + count);
double now = balance + count;
Thread.sleep(100); //存錢的操作用時0.1s
balance = now;
System.out.println("存錢線程:當前金額=" + balance);
}
取款方法:
public void withdraw(double count) throws InterruptedException{
System.out.println("取錢線程:取出金額=" + count);
double now = balance - count;
Thread.sleep(200); //取錢的操作用時0.2s
balance = now;
System.out.println("取錢線程:當前金額=" + balance);
}
}
現在,我們發現最終餘額變成了50,這很顯然是個完全不符合預期的錯誤結果。那麼,如何來解釋這個現象呢?
從上圖可以看到,出現數據不一致的原因在於多個線程並發訪問了同一個對象,破壞了不可分割的操作,這裏的這個共同訪問對象就是餘額。其實我們所謂預期的‘正確’結果,就是希望先進行存款,然後再進行取款,或者反之。
原子操作與鎖
上麵提到‘不可分割的操作’,這種操作就是原子操作。是因為實際上多線程編程的情境下,很多敏感數據不允許被同時訪問,因此對於這種針對敏感數據的操作,需要進行線程訪問的協調與控製,這就是所謂的線程同步(協同步調)訪問技術。線程同步控製的結果,就是把每次對敏感數據的操作變成原子操作,從而讓執行順序按照我們預期的過程進行。
上述情境下,存款與取款應當是兩個原子操作,我們必須保證先進行且完成存款操作再進行取款操作,才能保證最終數據的一致性,才能得到我們認為是‘正確’的結果。
下麵我們通過鎖來實現線程同步訪問控製,修改Card類的代碼如下。
public class Card {
private double balance;
private Object lock = new Object(); //鎖
...省略其它代碼
/*存款*/
public void deposit(double count) throws InterruptedException{
System.out.println("存錢線程:存入金額=" + count);
synchronized (lock) {
double now = balance + count;
Thread.sleep(100);//存錢的操作用時0.1s
balance = now;
}
System.out.println("存錢線程:當前金額=" + balance);
}
/*取款*/
public void withdraw(double count) throws InterruptedException{
System.out.println("取錢線程:取出金額=" + count);
synchronized (lock) {
double now = balance - count;
Thread.sleep(200);//取錢的操作用時0.2s
balance = now;
}
System.out.println("取錢線程:當前金額=" + balance);
}
}
這段代碼中,通過synchronized 關鍵字保證lock對象隻能同時被一個線程訪問,要想操作餘額,那麼必須先獲取lock對象的訪問許可,因此就保證了餘額不會被多個線程同時修改,而最終的結果也完全符合我們的預期。這個lock對象就可以形象的理解成鎖,整個執行過程大致如下圖所示,
最後更新:2017-08-20 18:08:45
上一篇:
[MySQL 5.7 metadata lock] 測試
下一篇:
數據庫和集合(MongoDB 文檔翻譯和解讀)
阿裏雲打造ET工業大腦 發揮“中國智造1%”的威力
編程之美之斐波那契數列
C++11中的mutex, lock,condition variable實現分析
Android 的不同尺寸圖片和布局(手機)
c語言基礎(四)之指針
tomcat中conf\Catalina\localhost目錄下的J2EE項目META-INF配置文件
Linux文件共享(三)——dup
#內含福利#2T免費的雲存儲,能帶我們飛多高?
Exception processing async thread queue
PostgreSQL 10.0 preview 功能增強 - 備庫支持邏輯訂閱, 支持訂閱漂移