class Account { String name; float amount; public Account(String name, float amount) { this.name = name; this.amount = amount; } public void deposit(float amt) { float tmp = amount; tmp += amt; try { Thread.sleep(100); //類比其它處理所需要的時間,比如重新整理資料庫等 } catch (InterruptedException e) { // ignore } amount = tmp; } public void withdraw(float amt) { float tmp = amount; tmp -= amt; try { Thread.sleep(100); //類比其它處理所需要的時間,比如重新整理資料庫等 } catch (InterruptedException e) { // ignore } amount = tmp; } public float getBalance() { return amount; } } public class AccountTest { private static int NUM_OF_THREAD = 1000; static Thread[] threads = new Thread[NUM_OF_THREAD]; public static void main(String[] args) { final Account acc = new Account("John", 1000.0f); for (int i = 0; i< NUM_OF_THREAD; i++) { threads[i] = new Thread(new Runnable() { public void run() { acc.deposit(100.0f); acc.withdraw(100.0f); } } ); threads[i].start(); } for (int i=0; i<NUM_OF_THREAD; i++) { try { threads[i].join(); //等待所有線程運行結束 } catch (InterruptedException e) { // ignore } } System.out.println("Finally, John's balance is:" + acc.getBalance()); }} 注意,上面在Account的deposit和withdraw方法中之所以要把對amount的運算使用一個臨時變數首先儲存,sleep一段時間,然後,再賦值給amount,是為了類比真實運行時的情況。因為在真實系統中,賬戶資訊肯定是儲存在持久媒介中,比如RDBMS中,此處的睡眠的時間相當於比較耗時的資料庫操作,最後把臨時變數tmp的值賦值給amount相當於把amount的改動寫入資料庫中。運行AccountTest,結果如下(每一次結果都會不同): E:/java/exer/bin>java AccountTest Finally, John's balance is:3900.0 E:/java/exer/bin>java AccountTest Finally, John's balance is:4900.0 E:/java/exer/bin>java AccountTest Finally, John's balance is:4700.0 E:/java/exer/bin>java AccountTest Finally, John's balance is:3900.0 E:/java/exer/bin>java AccountTest Finally, John's balance is:3900.0 E:/java/exer/bin>java AccountTest Finally, John's balance is:5200.0 為什麼會出現這樣的問題?這就是多線程中的同步的問題。在我們的程式中,Account中的amount會同時被多個線程所訪問,這就是一個競爭資源,通常稱作競態條件。對於這樣的多個線程共用的資源我們必須進行同步,以避免一個線程的改動被另一個線程所覆蓋。在我們這個程式中,Account中的amount是一個競態條件,所以所有對amount的修改訪問都要進行同步,我們將deposit()和withdraw()方法進行同步,修改為:
public synchronized void deposit(float amt) { float tmp = amount; tmp += amt; try { Thread.sleep(1); //類比其它處理所需要的時間,比如重新整理資料庫等 } catch (InterruptedException e) { // ignore } amount = tmp; } public synchronized void withdraw(float amt) { float tmp = amount; tmp -= amt; try { Thread.sleep(1); //類比其它處理所需要的時間,比如重新整理資料庫等 } catch (InterruptedException e) { // ignore } amount = tmp; } |
此時,再運行,我們就能夠得到正確的結果了。Account中的getBalance()也訪問了amount,為什麼不對getBalance()同步呢?因為getBalance()並不會修改amount的值,所以,同時多個線程對它訪問不會造成資料的混亂。 |