目录
概述
Java 提供了线程在处理公共资源时相互通信的机制。这篇文章给你一个线程通信的具体例子。
线程通信示例
让我们考虑一下这种情况。Alice 是一名计算机程序员,也喜欢购买新的闪亮的东西(手机、笔记本电脑、小工具)。她有一张要买的东西的清单。她在一家名为 MonkeyTypes Inc. 的公司工作。她的薪水是每月 1,000 美元。
想象一下,她目前有这个愿望清单:
- 新的 Macbook:3000 美元
- 一个新的机械键盘:400美元,
- 一部新手机:500美元,
- 一个新的闪亮小工具:500美元
她希望所有这些都没有特定的顺序。
她目前的余额是 0 美元。她想要的是,当她得到她的月薪时,她会把余额中的钱花在购买任何这些东西上。
让我们看看如何转换这个场景来演示线程通信。
代码实现
对于这个场景,让我们创建两个 Runnable 任务:一个模拟 Alice 的薪水,另一个模拟她的购买。
我们还需要一个类来代表她的银行账户。
让我们首先创建银行账户类:
class BankAccount {
private int balance = 0;
private static Lock lock = new ReentrantLock();
private static Condition paycheckArrivedCondition = lock.newCondition();
public void getPaid(int amount) {
lock.lock();
try {
System.out.println("Getting paid " + amount);
balance += amount;
paycheckArrivedCondition.signalAll();
} finally {
lock.unlock();
}
}
public void withdraw(int amount, String purpose) {
lock.lock();
try {
while (balance < amount) {
paycheckArrivedCondition.await();
}
System.out.println("Withdraw " + amount + " to " + purpose);
balance -= amount;
System.out.println("new balance -> " + balance);
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
如您所见,这个类有一个字段:balance
保持当前余额。此外,有两种方法可以在余额中存入和取款。
这里最有趣的细节是锁和条件。我在课程开始时创建了一个静态锁和一个静态条件。如您所知,锁有助于同步对天平的访问。另一方面,该条件有助于使线程之间的通信成为可能。
withdraw() 方法
在方法的开头,withdraw
调用lock
ReentrantLock 实例上的方法。这确保了只有拥有锁的线程才能执行该函数中的代码。
接下来,try/catch/finally 块确保锁在最后被释放。
while 循环检查余额是否有足够的钱,如果没有,则await
在条件实例上调用该函数。此调用释放锁。
deposit() 方法
与该withdraw()
方法类似,线程在这里执行代码也需要获取锁。signalAll
关于这个方法的一个有趣的事情是对条件实例上的方法的调用。这个调用是线程通信的核心。这会唤醒所有等待的线程,并重新开始检查余额 > 金额。
存钱的 Runnable 类
现在 BankAccount 类可用,让我们创建一个可运行的类来将钱存入 BankAccount 实例:
class PayEmployee implements Runnable {
private final BankAccount bankAccount;
private final int amount;
PayEmployee(BankAccount employeeBankAccount, int amount) {
this.bankAccount = employeeBankAccount;
this.amount = amount;
}
@Override
public void run() {
bankAccount.getPaid(amount);
}
}
Runnable 类取款
class BuyThings implements Runnable {
private final BankAccount bankAccount;
private final String purpose;
private final int amount;
public BuyThings(BankAccount account, String purpose, int amount) {
this.bankAccount = account;
this.purpose = purpose;
this.amount = amount;
System.out.println("Plan to " + purpose + " with " + amount);
}
@Override
public void run() {
bankAccount.withdraw(amount, purpose);
}
}
爱丽丝买东西在行动
现在让我们实现 Alice 提交愿望清单的代码。
public static void main(String[] args) {
BankAccount myAccount = new BankAccount();
var executors = Executors.newFixedThreadPool(5);
executors.submit(new BuyThings(myAccount, "buy new macbook pro", 3_000));
executors.submit(new BuyThings(myAccount, "buy new phone", 500));
executors.submit(new BuyThings(myAccount, "buy new keyboard", 400));
executors.submit(new BuyThings(myAccount, "buy new gadgets", 500));
int cycle = 6;
while (cycle > 0) {
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
executors.submit(new PayEmployee(myAccount, 1_000));
cycle--;
}
executors.shutdown();
}
从第 4 行到第 7 行,提交了她的所有购买。
从第 9 行到第 19 行,我模拟了她的付款。假设她与公司的合同只剩下 6 个月了。让我们运行程序并查看输出:
正如你所看到的,Alice 在拿到第一笔 1000 美元后,买了一部手机,然后买了一个新键盘……但是,这个顺序并不一致。下一次运行可能会产生不同的顺序。可以肯定的是,MacBook 总是最后购买的,因为只有在下一次付款后,Alice 才有足够的钱买得起。
你可能会问,如果 Alice 只剩下 4 个付款周期而不是 6 个呢?这意味着她永远没有足够的钱购买 MacBook。在这种情况下,程序将永远运行,因为购买 Macbook 线程一直在等待条件满足。(好伤心?)
使用监视器的线程通信
我已经向您介绍了使用 Lock 和 Condition 进行线程通信的概念。但是,这些类仅在 java 5 中可用。在此之前,使用了监视器。
什么是监视器?监视器是具有互斥和同步能力的常规对象。任何对象都可以是监视器。
回到上面的例子,我可以创建一个对象,而不是调用await
and signalAll
,将其用作监视器并调用wait
,notifyAll
以达到相同的结果。
这是上面使用监视器重写的示例:
class BankAccount {
private int balance = 0;
private static final Object monitor = new Object();
public void getPaid(int amount) {
synchronized (monitor) {
System.out.println("Getting paid " + amount);
balance += amount;
monitor.notifyAll();
}
}
public void withdraw(int amount, String purpose) {
synchronized (monitor) {
try {
while (balance < amount) {
monitor.wait();
}
System.out.println("Withdraw " + amount + " to " + purpose);
balance -= amount;
System.out.println("new balance -> " + balance);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
这里唯一的区别是我没有使用 Lock,而是创建了一个对象并使用它来同步对余额的访问。当余额不够时,我调用monitor.wait()
了所有的同步声明(类似于锁)。当有新的存款时,我打电话monitor.notifyAll
唤醒所有等待的线程。
这里重要的monitor
是一个静态实例,类似于第一个示例中的锁和条件。如果不是静态的,我最终会得到多个锁和监视器,这会使代码无法按预期工作。
结论
在这篇文章中,我向您介绍了 Java 中使用锁和条件进行线程通信的概念。线程可以获取锁,并检查条件是否满足。如果不满足条件,则await
对条件实例的调用会释放其他线程的锁。一个线程可以通过使用signalAll
(或signal
通知一个随机线程)来通知所有其他线程。
这篇文章的代码可以在Github上找到