【Java并发】线程通信

目录

概述

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调用lockReentrantLock 实例上的方法。这确保了只有拥有锁的线程才能执行该函数中的代码。

接下来,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 中可用。在此之前,使用了监视器。

什么是监视器?监视器是具有互斥和同步能力的常规对象。任何对象都可以是监视器。

回到上面的例子,我可以创建一个对象,而不是调用awaitand signalAll,将其用作监视器并调用waitnotifyAll以达到相同的结果。

这是上面使用监视器重写的示例:

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上找到