我們知道,線程的執行具有不確定性。因此,當我們需要控制線程的執行邏輯時,可以通過一些機制來保證線程的協調運行。Java提供瞭如下的幾個方法,來支持一些簡單的線程通信,他們都是屬於Object類的方法。
- wait():導致當前線程等待
- notify():通知喚醒其他的在此同步監視器上等待的單個線程,具有隨機選擇性
- notifyAll():通知喚醒其他的再次同步監視器上所有等待的線程
爲了做練習,我們使用最常見的銀行家示例來練習簡單的通信功能,參考《瘋狂Java講義》。
假設當前有一個賬戶,他必須先進行一次存款,才能進行一次取款操作,並且操作只能交替執行,不能重複執行。下面演示了這個例子如何完成。
1.銀行賬戶類
package com.money;
// 該程序控制取錢操作和存錢操作必須交替進行
public class Account {
private String accountNo;
private double balance;
private boolean flag = false;
public Account() {}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
// 取款
public synchronized void draw(double drawAmount) {
try {
// 表明還沒有存錢進來,則阻塞該線程
if(!flag) {
wait();
}else {
// 執行取錢操作
System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount);
balance -= drawAmount;
// 表明已取錢狀態
flag = false;
// 喚醒其他阻塞的線程
notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 存款
public synchronized void deposit(double depositAmount) {
try {
// 表明當前已經存過一次錢了,阻塞
if(flag) {
wait();
}else {
System.out.println(Thread.currentThread().getName()+ " 存款:" + depositAmount);
balance += depositAmount;
// 設置已存款狀態
flag = true;
// 喚醒其他線程
notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 餘額不允許隨便修改,因此只提供getter方法
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
2.存錢類(線程)
package com.money;
// 存錢線程
public class DepositThread extends Thread{
// 模擬用戶賬戶
private Account account;
// 當前線程所希望存錢的數量
private double depositAmount;
// 構造函數
public DepositThread(String name, Account account, double depositAmount) {
// 線程名稱
super(name);
// 賬戶信息
this.account = account;
// 取錢金額
this.depositAmount = depositAmount;
}
// 重複100次執行存錢操作
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(getName() + "(" + i + ")");
this.account.deposit(this.depositAmount);
}
System.out.println(getName() + "完成了所有的操作");
}
}
3.取錢類(線程)
package com.money;
// 取錢線程
public class DrawThread extends Thread{
// 模擬用戶賬戶
private Account account;
// 當前線程所希望取錢的數量
private double drawAmount;
// 構造函數
public DrawThread(String name, Account account, double drawAmount) {
// 線程名稱
super(name);
// 賬戶信息
this.account = account;
// 取錢金額
this.drawAmount = drawAmount;
}
// 重複100次執行取錢操作
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(getName() + "(" + i + ")");
this.account.draw(drawAmount);
}
System.out.println(getName() + "完成了所有的操作");
}
}
4.測試類
package com.money;
public class DrawTest {
public static void main(String[] args) {
// 創建一個賬戶
Account account = new Account("1016037677", 1000);
// 創建取錢、存錢用戶(線程)
new DrawThread("取錢者A", account, 50).start();
new DepositThread("存錢者B", account, 60).start();
new DepositThread("存錢者C", account, 60).start();
new DrawThread("取錢者D", account, 60).start();
}
}
5.輸出結果
注意其中flag旗幟參數,是用於表明當前存取錢狀態的參數,當我們存過一次錢之後將其設置爲true,以保證存錢操作進來時,若發現當前處於存錢狀態,則將線程設置爲等待狀態,只有當進行過一次取錢操作之後,該狀態才重新標記爲false,從而喚醒等待存錢的操作。
另外還要注意一點的是,我們在Account類的存、取錢方法上添加了synchronized關鍵字,用於表明此方法是一個同步方法,同步監視器爲當前對象,從而保證了資源不會被搶佔,這是多線程安全的基礎。
關於線程通信,還有其他的方法實現,例如Condition類等。
共勉。