16.6 线程通信 16.6.1 传统的线程通信

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,并调用notifynotifyAl()方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为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;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}

public String getAccountNo() {
return this.accountNo;
}

// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
// 取款功能
public synchronized void draw(double drawAmount) {
try {
// 如果flag为假,表明账户中还没有人存钱进去,我将取不到钱,先等别人存钱进去,取钱方法阻塞
if (!haveDeposit) {
wait();
}
// 如果旗标为真,表明有人存钱了,我就可以取钱
else {
// 执行取钱
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
haveDeposit = false;
// 唤醒其他线程
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}

// 存款功能
public synchronized void deposit(double depositAmount) {
try {
// 如果flag为真,表明账户中已有人存钱进去,不需要再存钱,等别人取走前我再存
if (haveDeposit) // ①
{
wait();
}
// 如果标记假,说明账户中的钱被取走了,那我就存钱进去
else {
// 执行存款
System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
haveDeposit = true;
// 唤醒其他线程
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
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()方法后,

  • 如果flagtrue,则表明账户中已有存款,暂时不需要我存款进去,程序调用wait()方法阻塞;
  • 如果flagfalse,则表示账户中没钱,需要存钱进去,程序向下执行存款操作,当存款操作执行完成后,将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;
}

// 重复100次执行取钱操作
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;
}

// 重复100次执行存款操作
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次尝试取钱操作,所以程序最后被阻塞.
这种阻塞并不是死锁,对于这种情况,取钱者线程已经执行结束,而存款者线程只是在等待其他线程来取钱而已,并不是等待其他线程释放冋步监视器。不要把死锁和程序阻塞等同起来。