1. 程式人生 > >java 銀行存取款模型的執行緒同步問題

java 銀行存取款模型的執行緒同步問題

關於執行緒同步,網上也有很多資料,不過不同的人理解也不大一樣,最近在研究這個問題的時候回想起大學課本上的一個經典模型,即銀行存取款模型,通過這個模型,我個人感覺解釋起來還是比較清楚的。本文結合自己的思考對該模型進行一個簡單的模擬,闡述一下我對執行緒同步的理解。

場景模擬

  接下來使用java對該問題進行模擬。在研究這個問題時會忽略掉現實系統中的很多其他屬性(官網:www.fhadmin.org),通過一個最簡單的餘額問題來看執行緒同步,這裡首先建立三個類。

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(官網:www.fhadmin.org)。

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());
    }
}

執行後輸出如下結果:
result1

  現在大致的看一下,初始餘額為100,然後存款執行緒存入100,接下來取款執行緒取走50,那麼最後餘額為150。這麼看來,貌似沒問題?

資料不一致問題

  事實上,存取款過程是需要消耗時間的(官網:www.fhadmin.org),只要一個執行緒在操作餘額期間受到其他執行緒的干擾,就可能出現數據不一致問題。這裡我們修改存取款方法的程式碼如下。

存款方法:

    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);
    }
}

然後再執行一遍測試程式:
result2

  現在,我們發現最終餘額變成了50,這很顯然是個完全不符合預期的錯誤結果。那麼,如何來解釋這個現象呢?
lock1
  從上圖可以看到,出現數據不一致的原因在於多個執行緒併發訪問了同一個物件,破壞了不可分割的操作,這裡的這個共同訪問物件就是餘額。其實我們所謂預期的‘正確’結果,就是希望先進行存款,然後再進行取款,或者反之。

原子操作與鎖

  上面提到‘不可分割的操作’,這種操作就是原子操作。是因為實際上多執行緒程式設計的情境下,很多敏感資料不允許被同時訪問,因此對於這種針對敏感資料的操作,需要進行執行緒訪問的協調與控制,這就是所謂的執行緒同步(協同步調)訪問技術。執行緒同步控制的結果,就是把每次對敏感資料的操作變成原子操作,從而讓執行順序按照我們預期的過程進行。
  上述情境下,存款與取款應當是兩個原子操作,我們必須保證先進行且完成存款操作再進行取款操作,才能保證最終資料的一致性,才能得到我們認為是‘正確’的結果。

下面我們通過鎖來實現執行緒同步訪問控制,修改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);
    }
}

執行結果如下:
result3

  這段程式碼中,通過synchronized 關鍵字保證lock物件只能同時被一個執行緒訪問,要想操作餘額,那麼必須先獲取lock物件的訪問許可,因此就保證了餘額不會被多個執行緒同時修改,而最終的結果也完全符合我們的預期。這個lock物件就可以形象的理解成鎖,整個執行過程大致如下圖所示,
lock2