16.6 线程通信
16.6.1 传统的线程通信
假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者。
现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
线程通信方法wait notify notifyAll
为了实现这种功能,可以借助于Object
类提供的wait()
、 notify()
和notifyAll()
三个方法,这三个方法并不属于Thread
类,而是属于Object
类。但这三个方法必须由同步监视器对象
来调用,这可分成以下两种情况。
- 对于使用
synchronized
修饰的同步代码块,同步监视器是synchronized
关键字后面括号里的对象,所以必须使用synchronized
关键字后面括号里的对象调用这三个方法.
- 对于使用
synchronized
修饰的同步方法,因为该类的默认实例(this
)就是同步监视器,所以可以在同步方法中直接调用这三个方法.
线程通信方法介绍
关于这三个方法的解释如下:
Object 类方法 |
描述 |
wait() |
导致当前线程等待,直到其他线程调用该同步监视器 的notify() 方法或notifyAll() 方法来唤醒该线程。 该wait() 方法有三种形式:- 无时间参数的
wait() 一直等待,直到其他线程通知)、 - 带毫秒参数的
wait() 和带毫秒、毫微秒参数的wait() 这两种方法都是等待指定时间后自动苏醒)。 调用wait() 方法的当前线程会释放对该同步监视器的锁定。 |
notify() |
唤醒在此同步监视器上等待的单个线程。 如果所有线程都在此同步监视器上等待,则会任意选择唤醒其中的一个线程。 但要等到当前线程放弃对该同步监视器的锁定后(使用wait() 方法),才可以执行被唤醒的线程。 |
notifyAll() |
唤醒在此同步监视器上等待的所有线程。 但要等到当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程 |
程序示例 账户类 存钱取钱的协调
程序中可以通过一个旗标来标识账户中是否已有存款:
- 当旗标为
false
时,表明账户中没有存款,存款者线程
可以向下执行,当存款者
把钱存入账户后,将旗标设为true
,并调用notify
(或notifyAll()
方法来唤醒其他线程;当存款者线程
进入线程体后,如果旗标为true
就调用wait()
方法让该线程等待。
- 当旗标为
true
时,表明账户中已经存入了存款,则取钱者线程
可以向下执行,当取钱者
把钱从账户中取出后,将旗标设为false
,并调用notify
或notifyAl()
方法来唤醒其他线程;当取钱者线程
进入线程体后,如果旗标为false
就调用wait()
方法让该线程等待。
修改账户类
本程序为Account
类提供draw()
和deposit()
两个方法,分别对应该账户的取钱、存款等操作,因为这两个方法可能需要并发修改Account
类的balance
成员变量的值,所以这两个方法都使用synchronized
修饰成同步方法。除此之外,这两个方法还使用了wait()
、notifyAll()
来控制线程的协作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| public class Account { private String accountNo; private double balance; private boolean haveDeposit = false;
public Account() { }
public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; }
public void setAccountNo(String accountNo) { this.accountNo = accountNo; }
public String getAccountNo() { return this.accountNo; }
public double getBalance() { return this.balance; } public synchronized void draw(double drawAmount) { try { if (!haveDeposit) { wait(); } else { System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); haveDeposit = false; notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } }
public synchronized void deposit(double depositAmount) { try { if (haveDeposit) { wait(); } else { System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); haveDeposit = true; notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } }
public int hashCode() { return accountNo.hashCode(); }
public boolean equals(Object obj) { if (this == obj) return true; if (obj != null && obj.getClass() == Account.class) { Account target = (Account) obj; return target.getAccountNo().equals(accountNo); } return false; } }
|
上面程序中使用wait()
和notifyAll()
进行了控制:
对于存款者线程
而言,当程序进入deposit()
方法后,
- 如果
flag
为true
,则表明账户中已有存款,暂时不需要我存款进去,程序调用wait()
方法阻塞;
- 如果
flag
为false
,则表示账户中没钱,需要存钱进去,程序向下执行存款操作,当存款操作执行完成后,将flag
设为true
,然后调用notifyAll()
方法来唤醒其他被阻塞的线程.
- 这样,如果系统中有存款者线程,
存款者线程
也会被唤醒,但该存款者线程执行到①号代码处时再次进入阻塞状态,
- 只有执行
draw()
方法的取钱者线程
才可以向下执行从而将钱取走。同理,取钱者线程的运行流程也是如此。
程序中的存款者线程循环100次重复存款,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用Account
对象的deposit()
、draw()
方法来实现。
取钱线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; }
public void run() { for (int i = 0; i < 100; i++) { account.draw(drawAmount); } } }
|
存钱线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; }
public void run() { for (int i = 0; i < 100; i++) { account.deposit(depositAmount); } } }
|
主程序
主程序可以启动任意多个存款线程和取钱线程,可以看到所有的取钱线程
必须等存款线程
存钱后才可以向下执行,而存款线程
也必须等取钱线程取钱
后才可以向下执行。主程序代码如下。
1 2 3 4 5 6 7 8 9 10
| public class DrawTest { public static void main(String[] args) { Account account = new Account("1234567", 0); new DrawThread("取钱者", account, 800).start(); new DepositThread("存款者甲", account, 800).start(); new DepositThread("存款者乙", account, 800).start(); new DepositThread("存款者丙", account, 800).start(); } }
|
运行效果
运行该程序,可以看到存款者线程、取钱者线程交替执行的情形,每当存款者向账户中存入800元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是800元,取钱结束后账户余额总是0元。运行该程序,会看到下所示的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 存款者甲 存款:800.0 账户余额为:800.0 取钱者 取钱:800.0 账户余额为:0.0 存款者丙 存款:800.0 账户余额为:800.0 取钱者 取钱:800.0 账户余额为:0.0 ...... 取钱者 取钱:800.0 账户余额为:0.0 存款者丙 存款:800.0 账户余额为:800.0 取钱者 取钱:800.0 账户余额为:0.0 存款者丙 存款:800.0 账户余额为:800.0
|
可以看出,3个存款者线程随机地向账户中存款,只有1个取钱者线程执行取钱操作。只有当取钱者取钱后,存款者才可以存款;同理,只有等存款者存款后,取钱者线程才可以取钱。
程序最后被阻塞无法继续向下执行,这是因为3个存款者线程共有300次尝试存款操作,但1个取钱者线程只有100次尝试取钱操作,所以程序最后被阻塞.
这种阻塞并不是死锁,对于这种情况,取钱者线程已经执行结束,而存款者线程只是在等待其他线程来取钱而已,并不是等待其他线程释放冋步监视器。不要把死锁和程序阻塞等同起来。