112年警察人員特種考試資訊管理人員三等物件導向程式設計
四、以下Java 程式模擬銀行帳戶存款與查詢餘額的多執行緒功能。Account 是帳戶類別,可以存款與查詢餘額;ATM 類別可以操作帳戶的存款功能;Test 類別輸出查詢之最後帳戶餘額。
(一)請說明此程式的執行緒之特性以及此程式輸出。(10分) (二)將 Line 28 註解掉後,請說明程式執行結果與其運作原因。再將 Line 13 註解掉,並打開 Line 14, 28 註解後,請說明此時程式執行結果與其運作原因。(15分) |
答:
import java.util.ArrayList; // 帳戶類別,包含存款 (deposit) 和取得餘額 (getBalance) 的方法 class Account { public void deposit(int m) { balance = balance + m; } // 存款方法,將傳入的金額加到餘額上 public int getBalance( ) { return balance; } // 取得餘額的方法,回傳當前的餘額 private int balance = 0; // 初始餘額為0 } // 自動提款機類別,繼承 Thread 類別,可以進行多執行緒操作 class ATM extends Thread { // 建構子,接收一個 Account 物件和一個金額作為存款 public ATM(Account a, int m) { account = a; money = m; } public void run( ) { // 覆寫 Thread 的 run 方法,會在呼叫 start( ) 時被執行 try { Thread.sleep(100); // 讓當前的執行緒睡眠100毫秒 } catch (InterruptedException e) { e.printStackTrace( ); // 捕捉和處理 InterruptedException 異常 } // 在同步區塊中進行存款操作,以確保資料的一致性 synchronized(Account.class) { account.deposit(money); } // account.deposit(money); } private Account account; // Account物件,代表一個帳戶 private int money; // 存款金額 } public class Test { public static void main(String[ ] args) throws InterruptedException { ArrayList<Thread> ts = new ArrayList<>( ); // 建立一個 Thread 物件的 ArrayList Account account = new Account( ); // 建立一個新的帳戶 for(int i = 1 ; i <= 10 ; i++) { // 重複10次 // 建立一個 ATM 物件,並且傳入帳戶和存款金額 (依序從1增加到10) Thread atm = new ATM(account, i); atm.start( ); // 開始 ATM 執行緒,會呼叫 ATM 的 run( ) 方法 ts.add(atm); // 將 ATM 物件加入 ArrayList 中 } for(Thread t:ts) { t.join( ); } // 使用 join( ) 方法等待所有的 ATM 執行緒結束 System.out.println("" + account.getBalance( )); // 輸出帳戶的最終餘額 } } |
執行結果:
55
說明:
這個程式建立了一個帳戶和10個自動提款機 (ATM)。每個 ATM 會向該帳戶存入一定的金額 (由1至10)。所有的 ATM 是在不同的執行緒中運行,因此它們的存款操作可能會同時進行。為了防止資料不一致 (race condition),在存款操作時使用了同步區塊 (synchronized) 來確保在同一時間只有一個執行緒可以操作帳戶。
(一)
1.執行緒的特性:
(1)此程式是一個具有多執行緒 (Thread) 的 Java 程式,其中每個 ATM 實例 是一個單獨的執行緒。在這個程式中,每個 ATM 執行緒會對同一個帳戶進行存款操作。考慮到多個執行緒可能同時進行存款操作,這裡使用了 Java 的 synchronized 關鍵字來確保一次只有一個執行緒可以存款,這樣就可以避免資料不一致的情況發生。
(2)這裡的synchronized(Account.class) 是一個同步區塊,代表一次只有一個執行緒可以進入此區塊的程式碼。由於存款操作在此同步區塊中,所以不會發生多個執行緒同時更改帳戶餘額的情況,這就確保了資料的一致性。
(3)最後,for(Thread t:ts) { t.join( ); } 確保主執行緒會等待所有 ATM 執行緒完成後再繼續執行。join( ) 方法會讓當前執行緒 (指主執行緒) 暫停執行,直到該執行緒 (指每個 ATM 執行緒) 結束。
2.程式輸出:
程式的輸出將會是所有 ATM 存款金額的總和,因為建立的 ATM 執行緒分別會向帳戶存入1到10的金額,所以輸出應該是1+2+3+...+10 = 55。
(二)
1.將Line 28註解掉後,程式執行結果與其運作原因:
(1)將 for(Thread t:ts) { t.join( ); } 註解掉後,會造成主執行緒可能不會等待所有的 ATM 執行緒結束就繼續執行。也就是說,當主執行緒執行到 System.out.println("" + account.getBalance( )); 時,可能還有一些 ATM 執行緒尚未完成存款操作。
(2)在多執行緒環境中,執行緒的執行順序是不確定的。如果不使用 join( ),那麼主執行緒並不會等待其他所有的 ATM 執行緒完成就會繼續執行。因此,System.out.println("" + account.getBalance( )); 可能在所有的存款操作完成之前就被執行,導致輸出的結果可能會小於55,即所有 ATM 存款的總和。
(3)為了確保所有的 ATM 執行緒都完成存款操作,通常會在主執行緒中使用 join( ) 方法來等待其他執行緒的結束。如果這裡不使用 join( ),那麼主執行緒可能會在所有的ATM 執行緒結束之前就輸出帳戶的餘額,這樣就可能獲得不正確的結果。
2.將Line 13註解掉,並打開Line 14, 28註解後,程式執行結果與其運作原因:
(1)將 synchronized(Account.class) { account.deposit(money); } 註解掉後,每個 ATM 執行緒執行存款操作時就不會進行同步了。這表示同一時間可能有多個 ATM 執行緒同時對帳戶進行存款操作,這就可能會導致資料的不一致性,也稱為競爭狀態 (Race Condition)。
(2)例如,假設兩個 ATM 執行緒同時進行存款操作,首先讀取帳戶的當前餘額,然後將存款金額加到當前餘額上,最後將新的餘額寫回帳戶。如果這兩個執行緒的這三個操作重疊了,那麼可能會發生下面的情況:
a.執行緒 A 及執行緒 B 讀取帳戶的當前餘額,假設都是10元。
b.執行緒 A 將存款金額加到當前餘額上,假設存款金額是5元,那麼新的餘額是15元。
c.執行緒 B 也將存款金額加到當前餘額上,假設存款金額是5元,但是它讀取的當前餘額還是10元,所以它計算出的新餘額也是15元。
d.執行緒 A 將新的餘額15元寫回帳戶。
e.執行緒 B 也將新的餘額15元寫回帳戶。
f.最終結果是帳戶的餘額是15元,但是實際上應該是20元 (原有的10元+執行緒 A 存款5元+執行緒 B 存款5元)。這就是因為競爭狀態導致的資料不一致。
(3)所以,如果將 synchronized(Account.class) { account.deposit(money); } 註解掉,程式輸出可能會小於55,這取決於執行緒的調度和執行順序。